├── .gitignore ├── .travis.yml ├── Example ├── .DS_Store ├── MBDocCapture.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── MBDocCapture-Example.xcscheme ├── MBDocCapture.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MBDocCapture │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ └── ViewController.swift ├── Podfile ├── Podfile.lock └── Pods │ ├── Local Podspecs │ └── MBDocCapture.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── Target Support Files │ ├── MBDocCapture │ ├── Info.plist │ ├── MBDocCapture-dummy.m │ ├── MBDocCapture-prefix.pch │ ├── MBDocCapture-umbrella.h │ ├── MBDocCapture.modulemap │ └── MBDocCapture.xcconfig │ ├── Pods-MBDocCapture_Example │ ├── Info.plist │ ├── Pods-MBDocCapture_Example-acknowledgements.markdown │ ├── Pods-MBDocCapture_Example-acknowledgements.plist │ ├── Pods-MBDocCapture_Example-dummy.m │ ├── Pods-MBDocCapture_Example-frameworks.sh │ ├── Pods-MBDocCapture_Example-resources.sh │ ├── Pods-MBDocCapture_Example-umbrella.h │ ├── Pods-MBDocCapture_Example.debug.xcconfig │ ├── Pods-MBDocCapture_Example.modulemap │ └── Pods-MBDocCapture_Example.release.xcconfig │ └── Pods-MBDocCapture_Tests │ ├── Info.plist │ ├── Pods-MBDocCapture_Tests-acknowledgements.markdown │ ├── Pods-MBDocCapture_Tests-acknowledgements.plist │ ├── Pods-MBDocCapture_Tests-dummy.m │ ├── Pods-MBDocCapture_Tests-frameworks.sh │ ├── Pods-MBDocCapture_Tests-resources.sh │ ├── Pods-MBDocCapture_Tests-umbrella.h │ ├── Pods-MBDocCapture_Tests.debug.xcconfig │ ├── Pods-MBDocCapture_Tests.modulemap │ └── Pods-MBDocCapture_Tests.release.xcconfig ├── LICENSE ├── MBDocCapture-demo.gif ├── MBDocCapture.podspec ├── MBDocCapture ├── Assets │ ├── .gitkeep │ ├── Icons │ │ ├── enhance │ │ │ ├── enhance.png │ │ │ ├── enhance@2x.png │ │ │ └── enhance@3x.png │ │ ├── rotate │ │ │ ├── rotate.png │ │ │ ├── rotate@2x.png │ │ │ └── rotate@3x.png │ │ └── touch │ │ │ └── ic_touch.png │ ├── en.lproj │ │ └── Localizable.strings │ └── fr.lproj │ │ └── Localizable.strings └── Classes │ ├── .gitkeep │ ├── Common │ ├── CIRectangleDetector.swift │ ├── EditScanCornerView.swift │ ├── Error.swift │ ├── Rectangle.swift │ └── RectangleView.swift │ ├── Extensions │ ├── AVCaptureVideoOrientation+Utils.swift │ ├── Array+Utils.swift │ ├── CGAffineTransform+Utils.swift │ ├── CGPoint+Utils.swift │ ├── CGRect+Utils.swift │ ├── CIImage+Utils.swift │ ├── UIColor+Utils.swift │ ├── UIImage+Orientation.swift │ └── UIImage+Utils.swift │ ├── Protocols │ ├── CaptureDevice.swift │ └── Transformable.swift │ ├── Scan │ ├── CaptureSessionManager.swift │ ├── FocusRectangleView.swift │ ├── RectangleFeaturesFunnel.swift │ └── ShutterButton.swift │ ├── Session │ ├── CaptureSession+Focus.swift │ ├── CaptureSession+Orientation.swift │ └── CaptureSession.swift │ └── ViewControllers │ ├── EditScanViewController.swift │ ├── ImageScannerController.swift │ ├── ReviewViewController.swift │ ├── ScannerViewController.swift │ └── ZoomGestureController.swift ├── README.md └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | Build/ 7 | Index/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | *.dSYM.zip 30 | *.dSYM 31 | 32 | ## Playgrounds 33 | timeline.xctimeline 34 | playground.xcworkspace 35 | 36 | # Swift Package Manager 37 | # 38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 39 | # Packages/ 40 | # Package.pins 41 | # Package.resolved 42 | .build/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | # Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots/**/*.png 69 | fastlane/test_output -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/MBDocCapture.xcworkspace -scheme MBDocCapture-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /Example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/Example/.DS_Store -------------------------------------------------------------------------------- /Example/MBDocCapture.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/MBDocCapture.xcodeproj/xcshareddata/xcschemes/MBDocCapture-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/MBDocCapture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/MBDocCapture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/MBDocCapture/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MBDocCapture 4 | // 5 | // Created by Mahdi on 04/16/2019. 6 | // Copyright (c) 2019 Mahdi. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Example/MBDocCapture/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/MBDocCapture/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/MBDocCapture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | The app requires access to the device's camera 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/MBDocCapture/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | 9 | import UIKit 10 | import MBDocCapture 11 | 12 | class ViewController: UIViewController { 13 | 14 | @IBOutlet weak var resultContainerView: UIView! 15 | @IBOutlet weak var page1Preview: UIImageView! 16 | @IBOutlet weak var page2Preview: UIImageView! 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | // Do any additional setup after loading the view, typically from a nib. 21 | } 22 | 23 | override func didReceiveMemoryWarning() { 24 | super.didReceiveMemoryWarning() 25 | // Dispose of any resources that can be recreated. 26 | } 27 | 28 | @IBAction func didSelectType1Button(_ sender: Any) { 29 | let scanner = ImageScannerController(delegate: self) 30 | scanner.shouldScanTwoFaces = false 31 | present(scanner, animated: true) 32 | } 33 | 34 | @IBAction func didSelectType2Button(_ sender: Any) { 35 | let scanner = ImageScannerController(delegate: self) 36 | scanner.shouldScanTwoFaces = true 37 | present(scanner, animated: true) 38 | } 39 | 40 | @IBAction func didSelectPreview1Button(_ sender: Any) { 41 | let scanner = ImageScannerController(image: page1Preview.image, delegate: self) 42 | present(scanner, animated: true) 43 | } 44 | 45 | @IBAction func didSelectPreview2Button(_ sender: Any) { 46 | let scanner = ImageScannerController(image: page2Preview.image, delegate: self) 47 | present(scanner, animated: true) 48 | } 49 | } 50 | 51 | extension ViewController: ImageScannerControllerDelegate { 52 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { 53 | scanner.dismiss(animated: true) { 54 | self.resultContainerView.isHidden = false 55 | self.page2Preview.isHidden = true 56 | 57 | if results.doesUserPreferEnhancedImage { 58 | self.page1Preview.image = results.enhancedImage 59 | } else { 60 | self.page1Preview.image = results.scannedImage 61 | } 62 | } 63 | } 64 | 65 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithPage1Results page1Results: ImageScannerResults, andPage2Results page2Results: ImageScannerResults) { 66 | scanner.dismiss(animated: true) { 67 | self.resultContainerView.isHidden = false 68 | self.page2Preview.isHidden = false 69 | 70 | if page1Results.doesUserPreferEnhancedImage { 71 | self.page1Preview.image = page1Results.enhancedImage 72 | } else { 73 | self.page1Preview.image = page1Results.scannedImage 74 | } 75 | 76 | if page2Results.doesUserPreferEnhancedImage { 77 | self.page2Preview.image = page2Results.enhancedImage 78 | } else { 79 | self.page2Preview.image = page2Results.scannedImage 80 | } 81 | } 82 | } 83 | 84 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { 85 | scanner.dismiss(animated: true) 86 | } 87 | 88 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { 89 | scanner.dismiss(animated: true) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | use_frameworks! 2 | 3 | target 'MBDocCapture_Example' do 4 | pod 'MBDocCapture', :path => '../' 5 | 6 | target 'MBDocCapture_Tests' do 7 | inherit! :search_paths 8 | 9 | 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MBDocCapture (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - MBDocCapture (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | MBDocCapture: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | MBDocCapture: 741a0cb836d185b4400c9fde6bc29688e4bc6c16 13 | 14 | PODFILE CHECKSUM: 289c8757248904e73f8439d279b4e92dec921cb5 15 | 16 | COCOAPODS: 1.5.3 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/MBDocCapture.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MBDocCapture", 3 | "version": "0.1.0", 4 | "summary": "A short description of MBDocCapture.", 5 | "description": "TODO: Add long description of the pod here.", 6 | "homepage": "https://github.com/Mahdi/MBDocCapture", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "Mahdi": "pqvf5779@win.mgt.w-ha.net" 13 | }, 14 | "source": { 15 | "git": "https://github.com/Mahdi/MBDocCapture.git", 16 | "tag": "0.1.0" 17 | }, 18 | "platforms": { 19 | "ios": "8.0" 20 | }, 21 | "source_files": "MBDocCapture/Classes/**/*" 22 | } 23 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - MBDocCapture (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - MBDocCapture (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | MBDocCapture: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | MBDocCapture: 741a0cb836d185b4400c9fde6bc29688e4bc6c16 13 | 14 | PODFILE CHECKSUM: 289c8757248904e73f8439d279b4e92dec921cb5 15 | 16 | COCOAPODS: 1.5.3 17 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/MBDocCapture-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_MBDocCapture : NSObject 3 | @end 4 | @implementation PodsDummy_MBDocCapture 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/MBDocCapture-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/MBDocCapture-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double MBDocCaptureVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char MBDocCaptureVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/MBDocCapture.modulemap: -------------------------------------------------------------------------------- 1 | framework module MBDocCapture { 2 | umbrella header "MBDocCapture-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/MBDocCapture/MBDocCapture.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## MBDocCapture 5 | 6 | Copyright (c) 2019 Mahdi 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | 26 | Generated by CocoaPods - https://cocoapods.org 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2019 Mahdi <pqvf5779@win.mgt.w-ha.net> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | 37 | License 38 | MIT 39 | Title 40 | MBDocCapture 41 | Type 42 | PSGroupSpecifier 43 | 44 | 45 | FooterText 46 | Generated by CocoaPods - https://cocoapods.org 47 | Title 48 | 49 | Type 50 | PSGroupSpecifier 51 | 52 | 53 | StringsTable 54 | Acknowledgements 55 | Title 56 | Acknowledgements 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_MBDocCapture_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_MBDocCapture_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 7 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # frameworks to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 13 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 14 | 15 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 16 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 17 | 18 | # Used as a return value for each invocation of `strip_invalid_archs` function. 19 | STRIP_BINARY_RETVAL=0 20 | 21 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 22 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 23 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 24 | 25 | # Copies and strips a vendored framework 26 | install_framework() 27 | { 28 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 29 | local source="${BUILT_PRODUCTS_DIR}/$1" 30 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 31 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 32 | elif [ -r "$1" ]; then 33 | local source="$1" 34 | fi 35 | 36 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 37 | 38 | if [ -L "${source}" ]; then 39 | echo "Symlinked..." 40 | source="$(readlink "${source}")" 41 | fi 42 | 43 | # Use filter instead of exclude so missing patterns don't throw errors. 44 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 45 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 46 | 47 | local basename 48 | basename="$(basename -s .framework "$1")" 49 | binary="${destination}/${basename}.framework/${basename}" 50 | if ! [ -r "$binary" ]; then 51 | binary="${destination}/${basename}" 52 | fi 53 | 54 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 55 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 56 | strip_invalid_archs "$binary" 57 | fi 58 | 59 | # Resign the code if required by the build settings to avoid unstable apps 60 | code_sign_if_enabled "${destination}/$(basename "$1")" 61 | 62 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 63 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 64 | local swift_runtime_libs 65 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 66 | for lib in $swift_runtime_libs; do 67 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 68 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 69 | code_sign_if_enabled "${destination}/${lib}" 70 | done 71 | fi 72 | } 73 | 74 | # Copies and strips a vendored dSYM 75 | install_dsym() { 76 | local source="$1" 77 | if [ -r "$source" ]; then 78 | # Copy the dSYM into a the targets temp dir. 79 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 80 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 81 | 82 | local basename 83 | basename="$(basename -s .framework.dSYM "$source")" 84 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 85 | 86 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 87 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 88 | strip_invalid_archs "$binary" 89 | fi 90 | 91 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 92 | # Move the stripped file into its final destination. 93 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 94 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 95 | else 96 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 97 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 98 | fi 99 | fi 100 | } 101 | 102 | # Signs a framework with the provided identity 103 | code_sign_if_enabled() { 104 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 105 | # Use the current code_sign_identitiy 106 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 107 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 108 | 109 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 110 | code_sign_cmd="$code_sign_cmd &" 111 | fi 112 | echo "$code_sign_cmd" 113 | eval "$code_sign_cmd" 114 | fi 115 | } 116 | 117 | # Strip invalid architectures 118 | strip_invalid_archs() { 119 | binary="$1" 120 | # Get architectures for current target binary 121 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 122 | # Intersect them with the architectures we are building for 123 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 124 | # If there are no archs supported by this binary then warn the user 125 | if [[ -z "$intersected_archs" ]]; then 126 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 127 | STRIP_BINARY_RETVAL=0 128 | return 129 | fi 130 | stripped="" 131 | for arch in $binary_archs; do 132 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 133 | # Strip non-valid architectures in-place 134 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 135 | stripped="$stripped $arch" 136 | fi 137 | done 138 | if [[ "$stripped" ]]; then 139 | echo "Stripped $binary of architectures:$stripped" 140 | fi 141 | STRIP_BINARY_RETVAL=1 142 | } 143 | 144 | 145 | if [[ "$CONFIGURATION" == "Debug" ]]; then 146 | install_framework "${BUILT_PRODUCTS_DIR}/MBDocCapture/MBDocCapture.framework" 147 | fi 148 | if [[ "$CONFIGURATION" == "Release" ]]; then 149 | install_framework "${BUILT_PRODUCTS_DIR}/MBDocCapture/MBDocCapture.framework" 150 | fi 151 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 152 | wait 153 | fi 154 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_MBDocCapture_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_MBDocCapture_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture/MBDocCapture.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "MBDocCapture" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_MBDocCapture_Example { 2 | umbrella header "Pods-MBDocCapture_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Example/Pods-MBDocCapture_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture/MBDocCapture.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "MBDocCapture" 7 | OTHER_SWIFT_FLAGS = $(inherited) "-D" "COCOAPODS" 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_MBDocCapture_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_MBDocCapture_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 7 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # frameworks to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 13 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 14 | 15 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 16 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 17 | 18 | # Used as a return value for each invocation of `strip_invalid_archs` function. 19 | STRIP_BINARY_RETVAL=0 20 | 21 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 22 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 23 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 24 | 25 | # Copies and strips a vendored framework 26 | install_framework() 27 | { 28 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 29 | local source="${BUILT_PRODUCTS_DIR}/$1" 30 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 31 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 32 | elif [ -r "$1" ]; then 33 | local source="$1" 34 | fi 35 | 36 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 37 | 38 | if [ -L "${source}" ]; then 39 | echo "Symlinked..." 40 | source="$(readlink "${source}")" 41 | fi 42 | 43 | # Use filter instead of exclude so missing patterns don't throw errors. 44 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 45 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 46 | 47 | local basename 48 | basename="$(basename -s .framework "$1")" 49 | binary="${destination}/${basename}.framework/${basename}" 50 | if ! [ -r "$binary" ]; then 51 | binary="${destination}/${basename}" 52 | fi 53 | 54 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 55 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 56 | strip_invalid_archs "$binary" 57 | fi 58 | 59 | # Resign the code if required by the build settings to avoid unstable apps 60 | code_sign_if_enabled "${destination}/$(basename "$1")" 61 | 62 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 63 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 64 | local swift_runtime_libs 65 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 66 | for lib in $swift_runtime_libs; do 67 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 68 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 69 | code_sign_if_enabled "${destination}/${lib}" 70 | done 71 | fi 72 | } 73 | 74 | # Copies and strips a vendored dSYM 75 | install_dsym() { 76 | local source="$1" 77 | if [ -r "$source" ]; then 78 | # Copy the dSYM into a the targets temp dir. 79 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 80 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 81 | 82 | local basename 83 | basename="$(basename -s .framework.dSYM "$source")" 84 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 85 | 86 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 87 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 88 | strip_invalid_archs "$binary" 89 | fi 90 | 91 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 92 | # Move the stripped file into its final destination. 93 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 94 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 95 | else 96 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 97 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 98 | fi 99 | fi 100 | } 101 | 102 | # Signs a framework with the provided identity 103 | code_sign_if_enabled() { 104 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 105 | # Use the current code_sign_identitiy 106 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 107 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 108 | 109 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 110 | code_sign_cmd="$code_sign_cmd &" 111 | fi 112 | echo "$code_sign_cmd" 113 | eval "$code_sign_cmd" 114 | fi 115 | } 116 | 117 | # Strip invalid architectures 118 | strip_invalid_archs() { 119 | binary="$1" 120 | # Get architectures for current target binary 121 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 122 | # Intersect them with the architectures we are building for 123 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 124 | # If there are no archs supported by this binary then warn the user 125 | if [[ -z "$intersected_archs" ]]; then 126 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 127 | STRIP_BINARY_RETVAL=0 128 | return 129 | fi 130 | stripped="" 131 | for arch in $binary_archs; do 132 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 133 | # Strip non-valid architectures in-place 134 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 135 | stripped="$stripped $arch" 136 | fi 137 | done 138 | if [[ "$stripped" ]]; then 139 | echo "Stripped $binary of architectures:$stripped" 140 | fi 141 | STRIP_BINARY_RETVAL=1 142 | } 143 | 144 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 145 | wait 146 | fi 147 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-resources.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH+x} ]; then 7 | # If UNLOCALIZED_RESOURCES_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # resources to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 13 | 14 | RESOURCES_TO_COPY=${PODS_ROOT}/resources-to-copy-${TARGETNAME}.txt 15 | > "$RESOURCES_TO_COPY" 16 | 17 | XCASSET_FILES=() 18 | 19 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 20 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 21 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 22 | 23 | case "${TARGETED_DEVICE_FAMILY:-}" in 24 | 1,2) 25 | TARGET_DEVICE_ARGS="--target-device ipad --target-device iphone" 26 | ;; 27 | 1) 28 | TARGET_DEVICE_ARGS="--target-device iphone" 29 | ;; 30 | 2) 31 | TARGET_DEVICE_ARGS="--target-device ipad" 32 | ;; 33 | 3) 34 | TARGET_DEVICE_ARGS="--target-device tv" 35 | ;; 36 | 4) 37 | TARGET_DEVICE_ARGS="--target-device watch" 38 | ;; 39 | *) 40 | TARGET_DEVICE_ARGS="--target-device mac" 41 | ;; 42 | esac 43 | 44 | install_resource() 45 | { 46 | if [[ "$1" = /* ]] ; then 47 | RESOURCE_PATH="$1" 48 | else 49 | RESOURCE_PATH="${PODS_ROOT}/$1" 50 | fi 51 | if [[ ! -e "$RESOURCE_PATH" ]] ; then 52 | cat << EOM 53 | error: Resource "$RESOURCE_PATH" not found. Run 'pod install' to update the copy resources script. 54 | EOM 55 | exit 1 56 | fi 57 | case $RESOURCE_PATH in 58 | *.storyboard) 59 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 60 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .storyboard`.storyboardc" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 61 | ;; 62 | *.xib) 63 | echo "ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile ${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib $RESOURCE_PATH --sdk ${SDKROOT} ${TARGET_DEVICE_ARGS}" || true 64 | ibtool --reference-external-strings-file --errors --warnings --notices --minimum-deployment-target ${!DEPLOYMENT_TARGET_SETTING_NAME} --output-format human-readable-text --compile "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename \"$RESOURCE_PATH\" .xib`.nib" "$RESOURCE_PATH" --sdk "${SDKROOT}" ${TARGET_DEVICE_ARGS} 65 | ;; 66 | *.framework) 67 | echo "mkdir -p ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 68 | mkdir -p "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 69 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" $RESOURCE_PATH ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" || true 70 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 71 | ;; 72 | *.xcdatamodel) 73 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH"`.mom\"" || true 74 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodel`.mom" 75 | ;; 76 | *.xcdatamodeld) 77 | echo "xcrun momc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd\"" || true 78 | xcrun momc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcdatamodeld`.momd" 79 | ;; 80 | *.xcmappingmodel) 81 | echo "xcrun mapc \"$RESOURCE_PATH\" \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm\"" || true 82 | xcrun mapc "$RESOURCE_PATH" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/`basename "$RESOURCE_PATH" .xcmappingmodel`.cdm" 83 | ;; 84 | *.xcassets) 85 | ABSOLUTE_XCASSET_FILE="$RESOURCE_PATH" 86 | XCASSET_FILES+=("$ABSOLUTE_XCASSET_FILE") 87 | ;; 88 | *) 89 | echo "$RESOURCE_PATH" || true 90 | echo "$RESOURCE_PATH" >> "$RESOURCES_TO_COPY" 91 | ;; 92 | esac 93 | } 94 | 95 | mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 96 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 97 | if [[ "${ACTION}" == "install" ]] && [[ "${SKIP_INSTALL}" == "NO" ]]; then 98 | mkdir -p "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 99 | rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${INSTALL_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 100 | fi 101 | rm -f "$RESOURCES_TO_COPY" 102 | 103 | if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ] 104 | then 105 | # Find all other xcassets (this unfortunately includes those of path pods and other targets). 106 | OTHER_XCASSETS=$(find "$PWD" -iname "*.xcassets" -type d) 107 | while read line; do 108 | if [[ $line != "${PODS_ROOT}*" ]]; then 109 | XCASSET_FILES+=("$line") 110 | fi 111 | done <<<"$OTHER_XCASSETS" 112 | 113 | if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then 114 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" 115 | else 116 | printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool --output-format human-readable-text --notices --warnings --platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" ${TARGET_DEVICE_ARGS} --compress-pngs --compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" --app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" --output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist" 117 | fi 118 | fi 119 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_MBDocCapture_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_MBDocCapture_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 4 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture/MBDocCapture.framework/Headers" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_MBDocCapture_Tests { 2 | umbrella header "Pods-MBDocCapture_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-MBDocCapture_Tests/Pods-MBDocCapture_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 4 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/MBDocCapture/MBDocCapture.framework/Headers" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 El Mahdi BOUKHRIS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MBDocCapture-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture-demo.gif -------------------------------------------------------------------------------- /MBDocCapture.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'MBDocCapture' 3 | spec.version = '0.1.4' 4 | spec.summary = 'MBDocCapture makes it easy to add document scanning functionalities to your iOS.' 5 | 6 | spec.description = <<-DESC 7 | MBDocCapture makes it easy to add document scanning functionalities to your iOS app but also image editing (Cropping and contrast enhacement). 8 | DESC 9 | 10 | 11 | spec.ios.deployment_target = '10.0' 12 | 13 | spec.homepage = 'https://github.com/iMhdi/MBDocCapture' 14 | spec.swift_version = '4.2' 15 | spec.license = { :type => 'MIT', :file => 'LICENSE' } 16 | spec.author = { 'El Mahdi BOUKHRIS' => 'm.boukhris@gmail.com' } 17 | spec.source = { :git => 'https://github.com/iMhdi/MBDocCapture.git', :tag => spec.version.to_s } 18 | 19 | spec.source_files = 'MBDocCapture/Classes/**/*' 20 | spec.resources = 'MBDocCapture/**/*.{strings,png}' 21 | 22 | spec.frameworks = 'CoreGraphics', 'CoreImage' 23 | end 24 | -------------------------------------------------------------------------------- /MBDocCapture/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/.gitkeep -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/enhance/enhance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/enhance/enhance.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/enhance/enhance@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/enhance/enhance@2x.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/enhance/enhance@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/enhance/enhance@3x.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/rotate/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/rotate/rotate.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/rotate/rotate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/rotate/rotate@2x.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/rotate/rotate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/rotate/rotate@3x.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/Icons/touch/ic_touch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Assets/Icons/touch/ic_touch.png -------------------------------------------------------------------------------- /MBDocCapture/Assets/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MBDocCapture 4 | 5 | Created by El Mahdi Boukhris on 16/04/2019. 6 | Copyright © 2019 El Mahdi Boukhris 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to 10 | deal in the Software without restriction, including without limitation the 11 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | sell copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /* The title on the navigation bar of the Edit screen. */ 28 | "mbdoccapture.scan_edit_title" = "Trimming"; 29 | 30 | /* The title on the navigation bar of the Review screen. */ 31 | "mbdoccapture.scan_review_title" = "Confirmation"; 32 | 33 | /* Flip Document Prompt message. */ 34 | "mbdoccapture.document_capture_flip" = "Flip your document and Touch the screen when you're ready to start the capture."; 35 | 36 | /* NavigationBar action titles */ 37 | "mbdoccapture.next_button" = "Next"; 38 | "mbdoccapture.cancel_button" = "Cancel"; 39 | -------------------------------------------------------------------------------- /MBDocCapture/Assets/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | MBDocCapture 4 | 5 | Created by El Mahdi Boukhris on 16/04/2019. 6 | Copyright © 2019 El Mahdi Boukhris 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to 10 | deal in the Software without restriction, including without limitation the 11 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | sell copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /* The title on the navigation bar of the Edit screen. */ 28 | "mbdoccapture.scan_edit_title" = "Recadrage"; 29 | 30 | /* The title on the navigation bar of the Review screen. */ 31 | "mbdoccapture.scan_review_title" = "Confirmation"; 32 | 33 | /* Flip Document Prompt message. */ 34 | "mbdoccapture.document_capture_flip" = "Retournez votre document et cliquez sur l'écran quand vous êtes prêt à démarrer la capture"; 35 | 36 | /* NavigationBar action titles */ 37 | "mbdoccapture.next_button" = "Suivant"; 38 | "mbdoccapture.cancel_button" = "Annuler"; 39 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iMhdi/MBDocCapture/044c72270089a07e7a3455298b71c98928d96392/MBDocCapture/Classes/.gitkeep -------------------------------------------------------------------------------- /MBDocCapture/Classes/Common/CIRectangleDetector.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleDetector.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import CoreImage 28 | import AVFoundation 29 | 30 | /// Class used to detect rectangles from an image. 31 | struct CIRectangleDetector { 32 | 33 | static let rectangleDetector = CIDetector(ofType: CIDetectorTypeRectangle, 34 | context: CIContext(options: nil), 35 | options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) 36 | 37 | /// Detects rectangles from the given image on iOS 10. 38 | /// 39 | /// - Parameters: 40 | /// - image: The image to detect rectangles on. 41 | /// - Returns: The biggest detected rectangle on the image. 42 | static func rectangle(forImage image: CIImage, completion: @escaping ((Rectangle?) -> Void)) { 43 | let biggestRectangle = rectangle(forImage: image) 44 | completion(biggestRectangle) 45 | } 46 | 47 | static func rectangle(forImage image: CIImage) -> Rectangle? { 48 | guard let rectangleFeatures = rectangleDetector?.features(in: image) as? [CIRectangleFeature] else { 49 | return nil 50 | } 51 | 52 | let rects = rectangleFeatures.map { rectangle in 53 | return Rectangle(rectangleFeature: rectangle) 54 | } 55 | 56 | return rects.biggest() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Common/EditScanCornerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditScanCornerView.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A UIView used by corners of a rectangle that is aware of its position. 30 | final class EditScanCornerView: UIView { 31 | 32 | let position: CornerPosition 33 | 34 | /// The image to display when the corner view is highlighted. 35 | private var image: UIImage? 36 | private(set) var isHighlighted = false 37 | 38 | lazy private var circleLayer: CAShapeLayer = { 39 | let layer = CAShapeLayer() 40 | layer.fillColor = UIColor.clear.cgColor 41 | layer.strokeColor = UIColor.white.cgColor 42 | layer.lineWidth = 1.0 43 | return layer 44 | }() 45 | 46 | init(frame: CGRect, position: CornerPosition) { 47 | self.position = position 48 | super.init(frame: frame) 49 | backgroundColor = UIColor.clear 50 | clipsToBounds = true 51 | layer.addSublayer(circleLayer) 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | override func layoutSubviews() { 59 | super.layoutSubviews() 60 | layer.cornerRadius = bounds.width / 2.0 61 | } 62 | 63 | override func draw(_ rect: CGRect) { 64 | super.draw(rect) 65 | 66 | let bezierPath = UIBezierPath(ovalIn: rect.insetBy(dx: circleLayer.lineWidth, dy: circleLayer.lineWidth)) 67 | circleLayer.frame = rect 68 | circleLayer.path = bezierPath.cgPath 69 | 70 | image?.draw(in: rect) 71 | } 72 | 73 | func highlightWithImage(_ image: UIImage) { 74 | isHighlighted = true 75 | self.image = image 76 | self.setNeedsDisplay() 77 | } 78 | 79 | func reset() { 80 | isHighlighted = false 81 | image = nil 82 | setNeedsDisplay() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Common/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// Errors related to the `ImageScannerController` 30 | public enum ImageScannerControllerError: Error { 31 | /// The user didn't grant permission to use the camera. 32 | case authorization 33 | /// An error occured when setting up the user's device. 34 | case inputDevice 35 | /// An error occured when trying to capture a picture. 36 | case capture 37 | /// Error when creating the CIImage. 38 | case ciImageCreation 39 | } 40 | 41 | extension ImageScannerControllerError: LocalizedError { 42 | 43 | public var errorDescription: String? { 44 | switch self { 45 | case .authorization: 46 | return "Failed to get the user's authorization for camera." 47 | case .inputDevice: 48 | return "Could not setup input device." 49 | case .capture: 50 | return "Could not capture pitcure." 51 | case .ciImageCreation: 52 | return "Internal Error - Could not create CIImage" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Common/Rectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rectangle.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | /// A data structure representing a rectangle and its position. This class exists to bypass the fact that CIRectangleFeature is read-only. 31 | public struct Rectangle: Transformable { 32 | 33 | /// A point that specifies the top left corner of the rectangle. 34 | public var topLeft: CGPoint 35 | 36 | /// A point that specifies the top right corner of the rectangle. 37 | public var topRight: CGPoint 38 | 39 | /// A point that specifies the bottom right corner of the rectangle. 40 | public var bottomRight: CGPoint 41 | 42 | /// A point that specifies the bottom left corner of the rectangle. 43 | public var bottomLeft: CGPoint 44 | 45 | init(rectangleFeature: CIRectangleFeature) { 46 | self.topLeft = rectangleFeature.topLeft 47 | self.topRight = rectangleFeature.topRight 48 | self.bottomLeft = rectangleFeature.bottomLeft 49 | self.bottomRight = rectangleFeature.bottomRight 50 | } 51 | 52 | init(topLeft: CGPoint, topRight: CGPoint, bottomRight: CGPoint, bottomLeft: CGPoint) { 53 | self.topLeft = topLeft 54 | self.topRight = topRight 55 | self.bottomRight = bottomRight 56 | self.bottomLeft = bottomLeft 57 | } 58 | 59 | public var description: String { 60 | return "topLeft: \(topLeft), topRight: \(topRight), bottomRight: \(bottomRight), bottomLeft: \(bottomLeft)" 61 | } 62 | 63 | /// The path of the Rectangle as a `UIBezierPath` 64 | var path: UIBezierPath { 65 | let path = UIBezierPath() 66 | path.move(to: topLeft) 67 | path.addLine(to: topRight) 68 | path.addLine(to: bottomRight) 69 | path.addLine(to: bottomLeft) 70 | path.close() 71 | 72 | return path 73 | } 74 | 75 | /// The perimeter of the Rectangle 76 | var perimeter: Double { 77 | let perimeter = topLeft.distanceTo(point: topRight) + topRight.distanceTo(point: bottomRight) + bottomRight.distanceTo(point: bottomLeft) + bottomLeft.distanceTo(point: topLeft) 78 | return Double(perimeter) 79 | } 80 | 81 | /// Applies a `CGAffineTransform` to the rectangle. 82 | /// 83 | /// - Parameters: 84 | /// - t: the transform to apply. 85 | /// - Returns: The transformed rectangle. 86 | func applying(_ transform: CGAffineTransform) -> Rectangle { 87 | let rectangle = Rectangle(topLeft: topLeft.applying(transform), topRight: topRight.applying(transform), bottomRight: bottomRight.applying(transform), bottomLeft: bottomLeft.applying(transform)) 88 | 89 | return rectangle 90 | } 91 | 92 | /// Checks whether the rectangle is withing a given distance of another rectangle. 93 | /// 94 | /// - Parameters: 95 | /// - distance: The distance (threshold) to use for the condition to be met. 96 | /// - rectangleFeature: The other rectangle to compare this instance with. 97 | /// - Returns: True if the given rectangle is within the given distance of this rectangle instance. 98 | func isWithin(_ distance: CGFloat, ofRectangleFeature rectangleFeature: Rectangle) -> Bool { 99 | 100 | let topLeftRect = topLeft.surroundingSquare(withSize: distance) 101 | if !topLeftRect.contains(rectangleFeature.topLeft) { 102 | return false 103 | } 104 | 105 | let topRightRect = topRight.surroundingSquare(withSize: distance) 106 | if !topRightRect.contains(rectangleFeature.topRight) { 107 | return false 108 | } 109 | 110 | let bottomRightRect = bottomRight.surroundingSquare(withSize: distance) 111 | if !bottomRightRect.contains(rectangleFeature.bottomRight) { 112 | return false 113 | } 114 | 115 | let bottomLeftRect = bottomLeft.surroundingSquare(withSize: distance) 116 | if !bottomLeftRect.contains(rectangleFeature.bottomLeft) { 117 | return false 118 | } 119 | 120 | return true 121 | } 122 | 123 | /// Reorganizes the current rectangle, making sure that the points are at their appropriate positions. For example, it ensures that the top left point is actually the top and left point point of the rectangle. 124 | mutating func reorganize() { 125 | let points = [topLeft, topRight, bottomRight, bottomLeft] 126 | let ySortedPoints = sortPointsByYValue(points) 127 | 128 | guard ySortedPoints.count == 4 else { 129 | return 130 | } 131 | 132 | let topMostPoints = Array(ySortedPoints[0..<2]) 133 | let bottomMostPoints = Array(ySortedPoints[2..<4]) 134 | let xSortedTopMostPoints = sortPointsByXValue(topMostPoints) 135 | let xSortedBottomMostPoints = sortPointsByXValue(bottomMostPoints) 136 | 137 | guard xSortedTopMostPoints.count > 1, 138 | xSortedBottomMostPoints.count > 1 else { 139 | return 140 | } 141 | 142 | topLeft = xSortedTopMostPoints[0] 143 | topRight = xSortedTopMostPoints[1] 144 | bottomRight = xSortedBottomMostPoints[1] 145 | bottomLeft = xSortedBottomMostPoints[0] 146 | } 147 | 148 | /// Scales the rectangle based on the ratio of two given sizes, and optionaly applies a rotation. 149 | /// 150 | /// - Parameters: 151 | /// - fromSize: The size the rectangle is currently related to. 152 | /// - toSize: The size to scale the rectangle to. 153 | /// - rotationAngle: The optional rotation to apply. 154 | /// - Returns: The newly scaled and potentially rotated rectangle. 155 | func scale(_ fromSize: CGSize, _ toSize: CGSize, withRotationAngle rotationAngle: CGFloat = 0.0) -> Rectangle { 156 | var invertedfromSize = fromSize 157 | let rotated = rotationAngle != 0.0 158 | 159 | if rotated && rotationAngle != CGFloat.pi { 160 | invertedfromSize = CGSize(width: fromSize.height, height: fromSize.width) 161 | } 162 | 163 | var transformedRect = self 164 | let invertedFromSizeWidth = invertedfromSize.width == 0 ? .leastNormalMagnitude : invertedfromSize.width 165 | 166 | let scale = toSize.width / invertedFromSizeWidth 167 | let scaledTransform = CGAffineTransform(scaleX: scale, y: scale) 168 | transformedRect = transformedRect.applying(scaledTransform) 169 | 170 | if rotated { 171 | let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle) 172 | 173 | let fromImageBounds = CGRect(origin: .zero, size: fromSize).applying(scaledTransform).applying(rotationTransform) 174 | 175 | let toImageBounds = CGRect(origin: .zero, size: toSize) 176 | let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: fromImageBounds, toCenterOfRect: toImageBounds) 177 | 178 | transformedRect = transformedRect.applyTransforms([rotationTransform, translationTransform]) 179 | } 180 | 181 | return transformedRect 182 | } 183 | 184 | // Convenience functions 185 | 186 | /// Sorts the given `CGPoints` based on their y value. 187 | /// - Parameters: 188 | /// - points: The poinmts to sort. 189 | /// - Returns: The points sorted based on their y value. 190 | private func sortPointsByYValue(_ points: [CGPoint]) -> [CGPoint] { 191 | return points.sorted { (point1, point2) -> Bool in 192 | point1.y < point2.y 193 | } 194 | } 195 | 196 | /// Sorts the given `CGPoints` based on their x value. 197 | /// - Parameters: 198 | /// - points: The points to sort. 199 | /// - Returns: The points sorted based on their x value. 200 | private func sortPointsByXValue(_ points: [CGPoint]) -> [CGPoint] { 201 | return points.sorted { (point1, point2) -> Bool in 202 | point1.x < point2.x 203 | } 204 | } 205 | } 206 | 207 | extension Rectangle { 208 | 209 | /// Converts the current to the cartesian coordinate system (where 0 on the y axis is at the bottom). 210 | /// 211 | /// - Parameters: 212 | /// - height: The height of the rect containing the rectangle. 213 | /// - Returns: The same rectangle in the cartesian coordinate system. 214 | func toCartesian(withHeight height: CGFloat) -> Rectangle { 215 | let topLeft = self.topLeft.cartesian(withHeight: height) 216 | let topRight = self.topRight.cartesian(withHeight: height) 217 | let bottomRight = self.bottomRight.cartesian(withHeight: height) 218 | let bottomLeft = self.bottomLeft.cartesian(withHeight: height) 219 | 220 | return Rectangle(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) 221 | } 222 | } 223 | 224 | extension Rectangle: Equatable { 225 | public static func == (lhs: Rectangle, rhs: Rectangle) -> Bool { 226 | return lhs.topLeft == rhs.topLeft && lhs.topRight == rhs.topRight && lhs.bottomRight == rhs.bottomRight && lhs.bottomLeft == rhs.bottomLeft 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Common/RectangleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleView.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import AVFoundation 29 | 30 | /// Simple enum to keep track of the position of the corners of a rectangle. 31 | enum CornerPosition { 32 | case topLeft 33 | case topRight 34 | case bottomRight 35 | case bottomLeft 36 | } 37 | 38 | /// The `RectangleView` is a simple `UIView` subclass that can draw a rectangle, and optionally edit it. 39 | final class RectangleView: UIView { 40 | 41 | private let rectLayer: CAShapeLayer = { 42 | let layer = CAShapeLayer() 43 | layer.strokeColor = UIColor.white.cgColor 44 | layer.lineWidth = 1.0 45 | layer.opacity = 1.0 46 | layer.isHidden = true 47 | 48 | return layer 49 | }() 50 | 51 | /// We want the corner views to be displayed under the outline of the rectangle. 52 | /// Because of that, we need the rectangle to be drawn on a UIView above them. 53 | private let rectView: UIView = { 54 | let view = UIView() 55 | view.backgroundColor = UIColor.clear 56 | view.translatesAutoresizingMaskIntoConstraints = false 57 | return view 58 | }() 59 | 60 | /// The rectangle drawn on the view. 61 | private(set) var rect: Rectangle? 62 | 63 | public var editable = false { 64 | didSet { 65 | cornerViews(hidden: !editable) 66 | rectLayer.fillColor = editable ? UIColor(white: 0.0, alpha: 0.6).cgColor : UIColor(white: 1.0, alpha: 0.5).cgColor 67 | guard let rect = rect else { 68 | return 69 | } 70 | drawRect(rect, animated: false) 71 | layoutCornerViews(forRect: rect) 72 | } 73 | } 74 | 75 | private var isHighlighted = false { 76 | didSet (oldValue) { 77 | guard oldValue != isHighlighted else { 78 | return 79 | } 80 | rectLayer.fillColor = isHighlighted ? UIColor.clear.cgColor : UIColor(white: 0.0, alpha: 0.6).cgColor 81 | isHighlighted ? bringSubviewToFront(rectView) : sendSubviewToBack(rectView) 82 | } 83 | } 84 | 85 | lazy private var topLeftCornerView: EditScanCornerView = { 86 | return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .topLeft) 87 | }() 88 | 89 | lazy private var topRightCornerView: EditScanCornerView = { 90 | return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .topRight) 91 | }() 92 | 93 | lazy private var bottomRightCornerView: EditScanCornerView = { 94 | return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .bottomRight) 95 | }() 96 | 97 | lazy private var bottomLeftCornerView: EditScanCornerView = { 98 | return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .bottomLeft) 99 | }() 100 | 101 | private let highlightedCornerViewSize = CGSize(width: 75.0, height: 75.0) 102 | private let cornerViewSize = CGSize(width: 20.0, height: 20.0) 103 | 104 | // MARK: - Life Cycle 105 | 106 | override init(frame: CGRect) { 107 | super.init(frame: frame) 108 | commonInit() 109 | } 110 | 111 | required public init?(coder aDecoder: NSCoder) { 112 | fatalError("init(coder:) has not been implemented") 113 | } 114 | 115 | private func commonInit() { 116 | addSubview(rectView) 117 | setupCornerViews() 118 | setupConstraints() 119 | rectView.layer.addSublayer(rectLayer) 120 | } 121 | 122 | private func setupConstraints() { 123 | let rectViewConstraints = [ 124 | rectView.topAnchor.constraint(equalTo: topAnchor), 125 | rectView.leadingAnchor.constraint(equalTo: leadingAnchor), 126 | bottomAnchor.constraint(equalTo: rectView.bottomAnchor), 127 | trailingAnchor.constraint(equalTo: rectView.trailingAnchor) 128 | ] 129 | 130 | NSLayoutConstraint.activate(rectViewConstraints) 131 | } 132 | 133 | private func setupCornerViews() { 134 | addSubview(topLeftCornerView) 135 | addSubview(topRightCornerView) 136 | addSubview(bottomRightCornerView) 137 | addSubview(bottomLeftCornerView) 138 | } 139 | 140 | override public func layoutSubviews() { 141 | super.layoutSubviews() 142 | guard rectLayer.frame != bounds else { 143 | return 144 | } 145 | 146 | rectLayer.frame = bounds 147 | if let rect = rect { 148 | drawRectangle(rect: rect, animated: false) 149 | } 150 | } 151 | 152 | // MARK: - Drawings 153 | 154 | /// Draws the passed in rectangle. 155 | /// 156 | /// - Parameters: 157 | /// - rect: The rectangle to draw on the view. It should be in the coordinates of the current `RectangleView` instance. 158 | func drawRectangle(rect: Rectangle, animated: Bool) { 159 | self.rect = rect 160 | drawRect(rect, animated: animated) 161 | if editable { 162 | cornerViews(hidden: false) 163 | layoutCornerViews(forRect: rect) 164 | } 165 | } 166 | 167 | private func drawRect(_ rect: Rectangle, animated: Bool) { 168 | var path = rect.path 169 | 170 | if editable { 171 | path = path.reversing() 172 | let rectPath = UIBezierPath(rect: bounds) 173 | path.append(rectPath) 174 | } 175 | 176 | if animated == true { 177 | let pathAnimation = CABasicAnimation(keyPath: "path") 178 | pathAnimation.duration = 0.2 179 | rectLayer.add(pathAnimation, forKey: "path") 180 | } 181 | 182 | rectLayer.path = path.cgPath 183 | rectLayer.isHidden = false 184 | } 185 | 186 | private func layoutCornerViews(forRect rect: Rectangle) { 187 | topLeftCornerView.center = rect.topLeft 188 | topRightCornerView.center = rect.topRight 189 | bottomLeftCornerView.center = rect.bottomLeft 190 | bottomRightCornerView.center = rect.bottomRight 191 | } 192 | 193 | func removeRectangle() { 194 | rectLayer.path = nil 195 | rectLayer.isHidden = true 196 | } 197 | 198 | // MARK: - Actions 199 | 200 | func moveCorner(cornerView: EditScanCornerView, atPoint point: CGPoint) { 201 | guard let rect = rect else { 202 | return 203 | } 204 | 205 | let validPoint = self.validPoint(point, forCornerViewOfSize: cornerView.bounds.size, inView: self) 206 | 207 | cornerView.center = validPoint 208 | let updatedRect = update(rect, withPosition: validPoint, forCorner: cornerView.position) 209 | 210 | self.rect = updatedRect 211 | drawRect(updatedRect, animated: false) 212 | } 213 | 214 | func highlightCornerAtPosition(position: CornerPosition, with image: UIImage) { 215 | guard editable else { 216 | return 217 | } 218 | isHighlighted = true 219 | 220 | let cornerView = cornerViewForCornerPosition(position: position) 221 | guard cornerView.isHighlighted == false else { 222 | cornerView.highlightWithImage(image) 223 | return 224 | } 225 | 226 | let origin = CGPoint(x: cornerView.frame.origin.x - (highlightedCornerViewSize.width - cornerViewSize.width) / 2.0, 227 | y: cornerView.frame.origin.y - (highlightedCornerViewSize.height - cornerViewSize.height) / 2.0) 228 | cornerView.frame = CGRect(origin: origin, size: highlightedCornerViewSize) 229 | cornerView.highlightWithImage(image) 230 | } 231 | 232 | func resetHighlightedCornerViews() { 233 | isHighlighted = false 234 | resetHighlightedCornerViews(cornerViews: [topLeftCornerView, topRightCornerView, bottomLeftCornerView, bottomRightCornerView]) 235 | } 236 | 237 | private func resetHighlightedCornerViews(cornerViews: [EditScanCornerView]) { 238 | cornerViews.forEach { (cornerView) in 239 | resetHightlightedCornerView(cornerView: cornerView) 240 | } 241 | } 242 | 243 | private func resetHightlightedCornerView(cornerView: EditScanCornerView) { 244 | cornerView.reset() 245 | let origin = CGPoint(x: cornerView.frame.origin.x + (cornerView.frame.size.width - cornerViewSize.width) / 2.0, 246 | y: cornerView.frame.origin.y + (cornerView.frame.size.height - cornerViewSize.width) / 2.0) 247 | cornerView.frame = CGRect(origin: origin, size: cornerViewSize) 248 | cornerView.setNeedsDisplay() 249 | } 250 | 251 | // MARK: Validation 252 | 253 | /// Ensures that the given point is valid - meaning that it is within the bounds of the passed in `UIView`. 254 | /// 255 | /// - Parameters: 256 | /// - point: The point that needs to be validated. 257 | /// - cornerViewSize: The size of the corner view representing the given point. 258 | /// - view: The view which should include the point. 259 | /// - Returns: A new point which is within the passed in view. 260 | private func validPoint(_ point: CGPoint, forCornerViewOfSize cornerViewSize: CGSize, inView view: UIView) -> CGPoint { 261 | var validPoint = point 262 | 263 | if point.x > view.bounds.width { 264 | validPoint.x = view.bounds.width 265 | } else if point.x < 0.0 { 266 | validPoint.x = 0.0 267 | } 268 | 269 | if point.y > view.bounds.height { 270 | validPoint.y = view.bounds.height 271 | } else if point.y < 0.0 { 272 | validPoint.y = 0.0 273 | } 274 | 275 | return validPoint 276 | } 277 | 278 | // MARK: - Convenience 279 | 280 | private func cornerViews(hidden: Bool) { 281 | topLeftCornerView.isHidden = hidden 282 | topRightCornerView.isHidden = hidden 283 | bottomRightCornerView.isHidden = hidden 284 | bottomLeftCornerView.isHidden = hidden 285 | } 286 | 287 | private func update(_ rect: Rectangle, withPosition position: CGPoint, forCorner corner: CornerPosition) -> Rectangle { 288 | var rect = rect 289 | 290 | switch corner { 291 | case .topLeft: 292 | rect.topLeft = position 293 | case .topRight: 294 | rect.topRight = position 295 | case .bottomRight: 296 | rect.bottomRight = position 297 | case .bottomLeft: 298 | rect.bottomLeft = position 299 | } 300 | 301 | return rect 302 | } 303 | 304 | func cornerViewForCornerPosition(position: CornerPosition) -> EditScanCornerView { 305 | switch position { 306 | case .topLeft: 307 | return topLeftCornerView 308 | case .topRight: 309 | return topRightCornerView 310 | case .bottomLeft: 311 | return bottomLeftCornerView 312 | case .bottomRight: 313 | return bottomRightCornerView 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/AVCaptureVideoOrientation+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIDeviceOrientation+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | extension AVCaptureVideoOrientation { 31 | 32 | /// Maps UIDeviceOrientation to AVCaptureVideoOrientation 33 | init?(deviceOrientation: UIDeviceOrientation) { 34 | switch deviceOrientation { 35 | case .portrait: 36 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) 37 | case .portraitUpsideDown: 38 | self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue) 39 | case .landscapeLeft: 40 | self.init(rawValue: AVCaptureVideoOrientation.landscapeLeft.rawValue) 41 | case .landscapeRight: 42 | self.init(rawValue: AVCaptureVideoOrientation.landscapeRight.rawValue) 43 | case .faceUp: 44 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) 45 | case .faceDown: 46 | self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue) 47 | default: 48 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/Array+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension Array where Element == Rectangle { 30 | 31 | /// Finds the biggest rectangle within an array of `Rectangle` objects. 32 | func biggest() -> Rectangle? { 33 | let biggestRectangle = self.max(by: { (rect1, rect2) -> Bool in 34 | return rect1.perimeter < rect2.perimeter 35 | }) 36 | 37 | return biggestRectangle 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/CGAffineTransform+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGAffineTransform+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import CoreGraphics 28 | 29 | extension CGAffineTransform { 30 | 31 | /// Convenience function to easily get a scale `CGAffineTransform` instance. 32 | /// 33 | /// - Parameters: 34 | /// - fromSize: The size that needs to be transformed to fit (aspect fill) in the other given size. 35 | /// - toSize: The size that should be matched by the `fromSize` parameter. 36 | /// - Returns: The transform that will make the `fromSize` parameter fir (aspect fill) inside the `toSize` parameter. 37 | static func scaleTransform(forSize fromSize: CGSize, aspectFillInSize toSize: CGSize) -> CGAffineTransform { 38 | let scale = max(toSize.width / fromSize.width, toSize.height / fromSize.height) 39 | return CGAffineTransform(scaleX: scale, y: scale) 40 | } 41 | 42 | /// Convenience function to easily get a translate `CGAffineTransform` instance. 43 | /// 44 | /// - Parameters: 45 | /// - fromRect: The rect which center needs to be translated to the center of the other passed in rect. 46 | /// - toRect: The rect that should be matched. 47 | /// - Returns: The transform that will translate the center of the `fromRect` parameter to the center of the `toRect` parameter. 48 | static func translateTransform(fromCenterOfRect fromRect: CGRect, toCenterOfRect toRect: CGRect) -> CGAffineTransform { 49 | let translate = CGPoint(x: toRect.midX - fromRect.midX, y: toRect.midY - fromRect.midY) 50 | return CGAffineTransform(translationX: translate.x, y: translate.y) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/CGPoint+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension CGPoint { 30 | 31 | /// Returns a rectangle of a given size surounding the point. 32 | /// 33 | /// - Parameters: 34 | /// - size: The size of the rectangle that should surround the points. 35 | /// - Returns: A `CGRect` instance that surrounds this instance of `CGpoint`. 36 | func surroundingSquare(withSize size: CGFloat) -> CGRect { 37 | return CGRect(x: x - size / 2.0, y: y - size / 2.0, width: size, height: size) 38 | } 39 | 40 | /// Checks wether this point is within a given distance of another point. 41 | /// 42 | /// - Parameters: 43 | /// - delta: The minimum distance to meet for this distance to return true. 44 | /// - point: The second point to compare this instance with. 45 | /// - Returns: True if the given `CGPoint` is within the given distance of this instance of `CGPoint`. 46 | func isWithin(delta: CGFloat, ofPoint point: CGPoint) -> Bool { 47 | return (abs(x - point.x) <= delta) && (abs(y - point.y) <= delta) 48 | } 49 | 50 | /// Returns the same `CGPoint` in the cartesian coordinate system. 51 | /// 52 | /// - Parameters: 53 | /// - height: The height of the bounds this points belong to, in the current coordinate system. 54 | /// - Returns: The same point in the cartesian coordinate system. 55 | func cartesian(withHeight height: CGFloat) -> CGPoint { 56 | return CGPoint(x: x, y: height - y) 57 | } 58 | 59 | /// Returns the distance between two points 60 | func distanceTo(point: CGPoint) -> CGFloat { 61 | return hypot((self.x - point.x), (self.y - point.y)) 62 | } 63 | 64 | /// Returns the closest corner from the point 65 | func closestCornerFrom(rect: Rectangle) -> CornerPosition { 66 | var smallestDistance = distanceTo(point: rect.topLeft) 67 | var closestCorner = CornerPosition.topLeft 68 | 69 | if distanceTo(point: rect.topRight) < smallestDistance { 70 | smallestDistance = distanceTo(point: rect.topRight) 71 | closestCorner = .topRight 72 | } 73 | 74 | if distanceTo(point: rect.bottomRight) < smallestDistance { 75 | smallestDistance = distanceTo(point: rect.bottomRight) 76 | closestCorner = .bottomRight 77 | } 78 | 79 | if distanceTo(point: rect.bottomLeft) < smallestDistance { 80 | smallestDistance = distanceTo(point: rect.bottomLeft) 81 | closestCorner = .bottomLeft 82 | } 83 | 84 | return closestCorner 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/CGRect+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGRect+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension CGRect { 30 | 31 | /// Returns a new `CGRect` instance scaled up or down, with the same center as the original `CGRect` instance. 32 | /// - Parameters: 33 | /// - ratio: The ratio to scale the `CGRect` instance by. 34 | /// - Returns: A new instance of `CGRect` scaled by the given ratio and centered with the original rect. 35 | func scaleAndCenter(withRatio ratio: CGFloat) -> CGRect { 36 | let scaleTransform = CGAffineTransform(scaleX: ratio, y: ratio) 37 | let scaledRect = applying(scaleTransform) 38 | 39 | let translateTransform = CGAffineTransform(translationX: origin.x * (1 - ratio) + (width - scaledRect.width) / 2.0, y: origin.y * (1 - ratio) + (height - scaledRect.height) / 2.0) 40 | let translatedRect = scaledRect.applying(translateTransform) 41 | 42 | return translatedRect 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/CIImage+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CIImage+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import CoreImage 28 | import UIKit 29 | 30 | extension CIImage { 31 | /// Applies an AdaptiveThresholding filter to the image, which enhances the image and makes it completely gray scale 32 | func applyingAdaptiveThreshold() -> UIImage? { 33 | guard let colorKernel = CIColorKernel(source: 34 | """ 35 | kernel vec4 color(__sample pixel, float inputEdgeO, float inputEdge1) 36 | { 37 | float luma = dot(pixel.rgb, vec3(0.2126, 0.7152, 0.0722)); 38 | float threshold = smoothstep(inputEdgeO, inputEdge1, luma); 39 | return vec4(threshold, threshold, threshold, 1.0); 40 | } 41 | """ 42 | ) else { return nil } 43 | 44 | let firstInputEdge = 0.25 45 | let secondInputEdge = 0.75 46 | 47 | let arguments: [Any] = [self, firstInputEdge, secondInputEdge] 48 | 49 | guard let enhancedCIImage = colorKernel.apply(extent: self.extent, arguments: arguments) else { return nil } 50 | 51 | if let cgImage = CIContext(options: nil).createCGImage(enhancedCIImage, from: enhancedCIImage.extent) { 52 | return UIImage(cgImage: cgImage) 53 | } else { 54 | return UIImage(ciImage: enhancedCIImage, scale: 1.0, orientation: .up) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/UIColor+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | extension UIColor { 30 | 31 | /// WHAColor: Create color object from Hex String 32 | /// 33 | /// - Parameter hexString: hex color code (ex : #FFFFFF) 34 | public convenience init(_ hexString: String?) { 35 | self.init(hexString: hexString, alpha: 1.0) 36 | } 37 | 38 | /// WHAColor: Create color object from Hex String 39 | /// 40 | /// - Parameter hexString: hex color code (ex : #FFFFFF) 41 | /// - Parameter alpha: alpha channel 42 | public convenience init(hexString: String?, alpha: Float = 1.0) { 43 | var red: CGFloat = 0 44 | var green: CGFloat = 0 45 | var blue: CGFloat = 0 46 | var mAlpha: CGFloat = CGFloat(alpha) 47 | var minusLength = 0 48 | 49 | guard (hexString != nil) else { 50 | self.init(red: red, green: green, blue: blue, alpha: 0) 51 | return 52 | } 53 | 54 | let scanner = Scanner(string: hexString!) 55 | 56 | if hexString!.hasPrefix("#") { 57 | scanner.scanLocation = 1 58 | minusLength = 1 59 | } 60 | if hexString!.hasPrefix("0x") { 61 | scanner.scanLocation = 2 62 | minusLength = 2 63 | } 64 | var hexValue: UInt64 = 0 65 | scanner.scanHexInt64(&hexValue) 66 | switch hexString!.count - minusLength { 67 | case 3: 68 | red = CGFloat((hexValue & 0xF00) >> 8) / 15.0 69 | green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0 70 | blue = CGFloat(hexValue & 0x00F) / 15.0 71 | case 4: 72 | red = CGFloat((hexValue & 0xF000) >> 12) / 15.0 73 | green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0 74 | blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0 75 | mAlpha = CGFloat(hexValue & 0x00F) / 15.0 76 | case 6: 77 | red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0 78 | green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0 79 | blue = CGFloat(hexValue & 0x0000FF) / 255.0 80 | case 8: 81 | red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0 82 | green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0 83 | blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0 84 | mAlpha = CGFloat(hexValue & 0x000000FF) / 255.0 85 | default: 86 | break 87 | } 88 | self.init(red: red, green: green, blue: blue, alpha: mAlpha) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/UIImage+Orientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Orientation.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension UIImage { 30 | 31 | /// Returns the same image with a portrait orientation. 32 | func applyingPortraitOrientation() -> UIImage { 33 | switch imageOrientation { 34 | case .up: 35 | return rotated(by: Measurement(value: Double.pi, unit: .radians), options: []) ?? self 36 | case .down: 37 | return rotated(by: Measurement(value: Double.pi, unit: .radians), options: [.flipOnVerticalAxis, .flipOnHorizontalAxis]) ?? self 38 | case .left: 39 | return self 40 | case .right: 41 | return rotated(by: Measurement(value: Double.pi / 2.0, unit: .radians), options: []) ?? self 42 | default: 43 | return self 44 | } 45 | } 46 | 47 | /// Data structure to easily express rotation options. 48 | struct RotationOptions: OptionSet { 49 | let rawValue: Int 50 | 51 | static let flipOnVerticalAxis = RotationOptions(rawValue: 1) 52 | static let flipOnHorizontalAxis = RotationOptions(rawValue: 2) 53 | } 54 | 55 | /// Rotate the image by the given angle, and perform other transformations based on the passed in options. 56 | /// 57 | /// - Parameters: 58 | /// - rotationAngle: The angle to rotate the image by. 59 | /// - options: Options to apply to the image. 60 | /// - Returns: The new image rotated and optentially flipped (@see options). 61 | func rotated(by rotationAngle: Measurement, options: RotationOptions = []) -> UIImage? { 62 | guard let cgImage = self.cgImage else { return nil } 63 | 64 | let rotationInRadians = CGFloat(rotationAngle.converted(to: .radians).value) 65 | let transform = CGAffineTransform(rotationAngle: rotationInRadians) 66 | let cgImageSize = CGSize(width: cgImage.width, height: cgImage.height) 67 | var rect = CGRect(origin: .zero, size: cgImageSize).applying(transform) 68 | rect.origin = .zero 69 | 70 | let format = UIGraphicsImageRendererFormat() 71 | format.scale = 1 72 | 73 | let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) 74 | 75 | let image = renderer.image { renderContext in 76 | renderContext.cgContext.translateBy(x: rect.midX, y: rect.midY) 77 | renderContext.cgContext.rotate(by: rotationInRadians) 78 | 79 | let x = options.contains(.flipOnVerticalAxis) ? -1.0 : 1.0 80 | let y = options.contains(.flipOnHorizontalAxis) ? 1.0 : -1.0 81 | renderContext.cgContext.scaleBy(x: CGFloat(x), y: CGFloat(y)) 82 | 83 | let drawRect = CGRect(origin: CGPoint(x: -cgImageSize.width / 2.0, y: -cgImageSize.height / 2.0), size: cgImageSize) 84 | renderContext.cgContext.draw(cgImage, in: drawRect) 85 | } 86 | 87 | return image 88 | } 89 | 90 | /// Rotates the image based on the information collected by the accelerometer 91 | func withFixedOrientation() -> UIImage { 92 | var imageAngle: Double = 0.0 93 | 94 | var shouldRotate = true 95 | switch CaptureSession.current.editImageOrientation { 96 | case .up: 97 | shouldRotate = false 98 | case .left: 99 | imageAngle = Double.pi / 2 100 | case .right: 101 | imageAngle = -(Double.pi / 2) 102 | case .down: 103 | imageAngle = Double.pi 104 | default: 105 | shouldRotate = false 106 | } 107 | 108 | if shouldRotate, 109 | let finalImage = rotated(by: Measurement(value: imageAngle, unit: .radians)) { 110 | return finalImage 111 | } else { 112 | return self 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Extensions/UIImage+Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Utils.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | extension UIImage { 30 | 31 | /// Draws a new cropped and scaled (zoomed in) image. 32 | /// 33 | /// - Parameters: 34 | /// - point: The center of the new image. 35 | /// - scaleFactor: Factor by which the image should be zoomed in. 36 | /// - size: The size of the rect the image will be displayed in. 37 | /// - Returns: The scaled and cropped image. 38 | func scaledImage(atPoint point: CGPoint, scaleFactor: CGFloat, targetSize size: CGSize) -> UIImage? { 39 | 40 | guard let cgImage = self.cgImage else { 41 | return nil 42 | } 43 | 44 | let scaledSize = CGSize(width: size.width / scaleFactor, height: size.height / scaleFactor) 45 | let midX = point.x - scaledSize.width / 2.0 46 | let midY = point.y - scaledSize.height / 2.0 47 | let newRect = CGRect(x: midX, y: midY, width: scaledSize.width, height: scaledSize.height) 48 | 49 | guard let croppedImage = cgImage.cropping(to: newRect) else { 50 | return nil 51 | } 52 | 53 | return UIImage(cgImage: croppedImage) 54 | } 55 | 56 | /// Function gathered from [here](https://stackoverflow.com/questions/44462087/how-to-convert-a-uiimage-to-a-cvpixelbuffer) to convert UIImage to CVPixelBuffer 57 | /// 58 | /// - Returns: new [CVPixelBuffer](apple-reference-documentation://hsVf8OXaJX) 59 | func pixelBuffer() -> CVPixelBuffer? { 60 | let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary 61 | var pixelBufferOpt: CVPixelBuffer? 62 | let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(self.size.width), Int(self.size.height), kCVPixelFormatType_32ARGB, attrs, &pixelBufferOpt) 63 | guard status == kCVReturnSuccess, let pixelBuffer = pixelBufferOpt else { 64 | return nil 65 | } 66 | 67 | CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) 68 | let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) 69 | 70 | let rgbColorSpace = CGColorSpaceCreateDeviceRGB() 71 | guard let context = CGContext(data: pixelData, width: Int(self.size.width), height: Int(self.size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: rgbColorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue) else { 72 | return nil 73 | } 74 | 75 | context.translateBy(x: 0, y: self.size.height) 76 | context.scaleBy(x: 1.0, y: -1.0) 77 | 78 | UIGraphicsPushContext(context) 79 | self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) 80 | UIGraphicsPopContext() 81 | CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) 82 | 83 | return pixelBuffer 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Protocols/CaptureDevice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureDevice.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | protocol CaptureDevice: class { 31 | func unlockForConfiguration() 32 | func lockForConfiguration() throws 33 | 34 | var torchMode: AVCaptureDevice.TorchMode { get set } 35 | var isTorchAvailable: Bool { get } 36 | 37 | var focusMode: AVCaptureDevice.FocusMode { get set } 38 | var focusPointOfInterest: CGPoint { get set } 39 | var isFocusPointOfInterestSupported: Bool { get } 40 | func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool 41 | 42 | var exposureMode: AVCaptureDevice.ExposureMode { get set } 43 | var exposurePointOfInterest: CGPoint { get set } 44 | var isExposurePointOfInterestSupported: Bool { get } 45 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool 46 | 47 | var isSubjectAreaChangeMonitoringEnabled: Bool { get set } 48 | } 49 | 50 | extension AVCaptureDevice: CaptureDevice { } 51 | 52 | final class MockCaptureDevice: CaptureDevice { 53 | func unlockForConfiguration() { 54 | return 55 | } 56 | 57 | func lockForConfiguration() throws { 58 | return 59 | } 60 | 61 | var torchMode: AVCaptureDevice.TorchMode = .off 62 | var isTorchAvailable: Bool = true 63 | 64 | var focusMode: AVCaptureDevice.FocusMode = .continuousAutoFocus 65 | var focusPointOfInterest: CGPoint = .zero 66 | var isFocusPointOfInterestSupported: Bool = true 67 | 68 | var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure 69 | var exposurePointOfInterest: CGPoint = .zero 70 | var isExposurePointOfInterestSupported: Bool = true 71 | 72 | func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool { 73 | return true 74 | } 75 | 76 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool { 77 | return true 78 | } 79 | 80 | var isSubjectAreaChangeMonitoringEnabled: Bool = false 81 | } 82 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Protocols/Transformable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extendable.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// Objects that conform to the Transformable protocol are capable of being transformed with a `CGAffineTransform`. 30 | protocol Transformable { 31 | 32 | /// Applies the given `CGAffineTransform`. 33 | /// 34 | /// - Parameters: 35 | /// - t: The transform to apply 36 | /// - Returns: The same object transformed by the passed in `CGAffineTransform`. 37 | func applying(_ transform: CGAffineTransform) -> Self 38 | 39 | } 40 | 41 | extension Transformable { 42 | 43 | /// Applies multiple given transforms in the given order. 44 | /// 45 | /// - Parameters: 46 | /// - transforms: The transforms to apply. 47 | /// - Returns: The same object transformed by the passed in `CGAffineTransform`s. 48 | func applyTransforms(_ transforms: [CGAffineTransform]) -> Self { 49 | 50 | var transformableObject = self 51 | 52 | transforms.forEach { (transform) in 53 | transformableObject = transformableObject.applying(transform) 54 | } 55 | 56 | return transformableObject 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Scan/CaptureSessionManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureManager.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import CoreMotion 29 | import CoreImage 30 | import UIKit 31 | import AVFoundation 32 | 33 | /// A set of functions that inform the delegate object of the state of the detection. 34 | protocol RectangleDetectionDelegateProtocol: NSObjectProtocol { 35 | 36 | /// Called when the capture of a picture has started. 37 | /// 38 | /// - Parameters: 39 | /// - captureSessionManager: The `CaptureSessionManager` instance that started capturing a picture. 40 | func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager) 41 | 42 | /// Called when a rectangle has been detected. 43 | /// - Parameters: 44 | /// - captureSessionManager: The `CaptureSessionManager` instance that has detected a rectangle. 45 | /// - rect: The detected rectangle in the coordinates of the image. 46 | /// - imageSize: The size of the image the rectangle has been detected on. 47 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didDetectRect rect: Rectangle?, _ imageSize: CGSize) 48 | 49 | /// Called when a picture with or without a rectangle has been captured. 50 | /// 51 | /// - Parameters: 52 | /// - captureSessionManager: The `CaptureSessionManager` instance that has captured a picture. 53 | /// - picture: The picture that has been captured. 54 | /// - rect: The rectangle that was detected in the picture's coordinates if any. 55 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didCapturePicture picture: UIImage, withRect rect: Rectangle?) 56 | 57 | /// Called when an error occured with the capture session manager. 58 | /// - Parameters: 59 | /// - captureSessionManager: The `CaptureSessionManager` that encountered an error. 60 | /// - error: The encountered error. 61 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error) 62 | } 63 | 64 | /// The CaptureSessionManager is responsible for setting up and managing the AVCaptureSession and the functions related to capturing. 65 | final class CaptureSessionManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { 66 | 67 | private let videoPreviewLayer: AVCaptureVideoPreviewLayer 68 | private let captureSession = AVCaptureSession() 69 | private let rectangleFunnel = RectangleFeaturesFunnel() 70 | weak var delegate: RectangleDetectionDelegateProtocol? 71 | private var displayedRectangleResult: RectangleDetectorResult? 72 | private var photoOutput = AVCapturePhotoOutput() 73 | 74 | /// Whether the CaptureSessionManager should be detecting rectangles. 75 | private var isDetecting = true 76 | 77 | /// The number of times no rectangles have been found in a row. 78 | private var noRectangleCount = 0 79 | 80 | /// The minimum number of time required by `noRectangleCount` to validate that no rectangles have been found. 81 | private let noRectangleThreshold = 3 82 | 83 | // MARK: Life Cycle 84 | 85 | init?(videoPreviewLayer: AVCaptureVideoPreviewLayer) { 86 | self.videoPreviewLayer = videoPreviewLayer 87 | super.init() 88 | 89 | guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { 90 | let error = ImageScannerControllerError.inputDevice 91 | delegate?.captureSessionManager(self, didFailWithError: error) 92 | return nil 93 | } 94 | 95 | captureSession.beginConfiguration() 96 | captureSession.sessionPreset = AVCaptureSession.Preset.photo 97 | 98 | photoOutput.isHighResolutionCaptureEnabled = true 99 | 100 | let videoOutput = AVCaptureVideoDataOutput() 101 | videoOutput.alwaysDiscardsLateVideoFrames = true 102 | 103 | defer { 104 | device.unlockForConfiguration() 105 | captureSession.commitConfiguration() 106 | } 107 | 108 | guard let deviceInput = try? AVCaptureDeviceInput(device: device), 109 | captureSession.canAddInput(deviceInput), 110 | captureSession.canAddOutput(photoOutput), 111 | captureSession.canAddOutput(videoOutput) else { 112 | let error = ImageScannerControllerError.inputDevice 113 | delegate?.captureSessionManager(self, didFailWithError: error) 114 | return 115 | } 116 | 117 | do { 118 | try device.lockForConfiguration() 119 | } catch { 120 | let error = ImageScannerControllerError.inputDevice 121 | delegate?.captureSessionManager(self, didFailWithError: error) 122 | return 123 | } 124 | 125 | device.isSubjectAreaChangeMonitoringEnabled = true 126 | 127 | captureSession.addInput(deviceInput) 128 | captureSession.addOutput(photoOutput) 129 | captureSession.addOutput(videoOutput) 130 | 131 | videoPreviewLayer.session = captureSession 132 | videoPreviewLayer.videoGravity = .resizeAspectFill 133 | 134 | videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video_ouput_queue")) 135 | } 136 | 137 | // MARK: Capture Session Life Cycle 138 | 139 | /// Starts the camera and detecting rectangles. 140 | internal func start() { 141 | let authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) 142 | 143 | switch authorizationStatus { 144 | case .authorized: 145 | DispatchQueue.main.async { 146 | self.captureSession.startRunning() 147 | } 148 | isDetecting = true 149 | case .notDetermined: 150 | AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (_) in 151 | DispatchQueue.main.async { [weak self] in 152 | self?.start() 153 | } 154 | }) 155 | default: 156 | let error = ImageScannerControllerError.authorization 157 | delegate?.captureSessionManager(self, didFailWithError: error) 158 | } 159 | } 160 | 161 | internal func stop() { 162 | captureSession.stopRunning() 163 | } 164 | 165 | internal func capturePhoto() { 166 | guard let connection = photoOutput.connection(with: .video), connection.isEnabled, connection.isActive else { 167 | let error = ImageScannerControllerError.capture 168 | delegate?.captureSessionManager(self, didFailWithError: error) 169 | return 170 | } 171 | 172 | let photoSettings = AVCapturePhotoSettings() 173 | photoSettings.isHighResolutionPhotoEnabled = true 174 | photoSettings.isAutoStillImageStabilizationEnabled = true 175 | 176 | photoOutput.capturePhoto(with: photoSettings, delegate: self) 177 | } 178 | 179 | // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate 180 | 181 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 182 | guard isDetecting == true, 183 | let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 184 | return 185 | } 186 | 187 | let imageSize = CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)) 188 | 189 | let finalImage = CIImage(cvPixelBuffer: pixelBuffer) 190 | CIRectangleDetector.rectangle(forImage: finalImage) { (rectangle) in 191 | self.processRectangle(rectangle: rectangle, imageSize: imageSize) 192 | } 193 | } 194 | 195 | private func processRectangle(rectangle: Rectangle?, imageSize: CGSize) { 196 | if let rectangle = rectangle { 197 | 198 | self.noRectangleCount = 0 199 | self.rectangleFunnel.add(rectangle, currentlyDisplayedRectangle: self.displayedRectangleResult?.rectangle) { [weak self] (result, rectangle) in 200 | 201 | guard let strongSelf = self else { 202 | return 203 | } 204 | 205 | let shouldAutoScan = (result == .showAndAutoScan) 206 | strongSelf.displayRectangleResult(rectangleResult: RectangleDetectorResult(rectangle: rectangle, imageSize: imageSize)) 207 | if shouldAutoScan, CaptureSession.current.isAutoScanEnabled, !CaptureSession.current.isEditing { 208 | capturePhoto() 209 | } 210 | } 211 | 212 | } else { 213 | 214 | DispatchQueue.main.async { [weak self] in 215 | guard let strongSelf = self else { 216 | return 217 | } 218 | strongSelf.noRectangleCount += 1 219 | 220 | if strongSelf.noRectangleCount > strongSelf.noRectangleThreshold { 221 | // Reset the currentAutoScanPassCount, so the threshold is restarted the next time a rectangle is found 222 | strongSelf.rectangleFunnel.currentAutoScanPassCount = 0 223 | 224 | // Remove the currently displayed rectangle as no rectangles are being found anymore 225 | strongSelf.displayedRectangleResult = nil 226 | strongSelf.delegate?.captureSessionManager(strongSelf, didDetectRect: nil, imageSize) 227 | } 228 | } 229 | return 230 | 231 | } 232 | } 233 | 234 | @discardableResult private func displayRectangleResult(rectangleResult: RectangleDetectorResult) -> Rectangle { 235 | displayedRectangleResult = rectangleResult 236 | 237 | let rect = rectangleResult.rectangle.toCartesian(withHeight: rectangleResult.imageSize.height) 238 | 239 | DispatchQueue.main.async { [weak self] in 240 | guard let strongSelf = self else { 241 | return 242 | } 243 | 244 | strongSelf.delegate?.captureSessionManager(strongSelf, didDetectRect: rect, rectangleResult.imageSize) 245 | } 246 | 247 | return rect 248 | } 249 | } 250 | 251 | extension CaptureSessionManager: AVCapturePhotoCaptureDelegate { 252 | 253 | func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) { 254 | if let error = error { 255 | delegate?.captureSessionManager(self, didFailWithError: error) 256 | return 257 | } 258 | 259 | CaptureSession.current.setImageOrientation() 260 | 261 | isDetecting = false 262 | rectangleFunnel.currentAutoScanPassCount = 0 263 | delegate?.didStartCapturingPicture(for: self) 264 | 265 | if let sampleBuffer = photoSampleBuffer, 266 | let imageData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: sampleBuffer, previewPhotoSampleBuffer: nil) { 267 | completeImageCapture(with: imageData) 268 | } else { 269 | let error = ImageScannerControllerError.capture 270 | delegate?.captureSessionManager(self, didFailWithError: error) 271 | return 272 | } 273 | 274 | } 275 | 276 | @available(iOS 11.0, *) 277 | func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { 278 | if let error = error { 279 | delegate?.captureSessionManager(self, didFailWithError: error) 280 | return 281 | } 282 | 283 | CaptureSession.current.setImageOrientation() 284 | 285 | isDetecting = false 286 | rectangleFunnel.currentAutoScanPassCount = 0 287 | delegate?.didStartCapturingPicture(for: self) 288 | 289 | if let imageData = photo.fileDataRepresentation() { 290 | completeImageCapture(with: imageData) 291 | } else { 292 | let error = ImageScannerControllerError.capture 293 | delegate?.captureSessionManager(self, didFailWithError: error) 294 | return 295 | } 296 | } 297 | 298 | /// Completes the image capture by processing the image, and passing it to the delegate object. 299 | /// This function is necessary because the capture functions for iOS 10 and 11 are decoupled. 300 | private func completeImageCapture(with imageData: Data) { 301 | DispatchQueue.global(qos: .background).async { [weak self] in 302 | CaptureSession.current.isEditing = true 303 | guard let image = UIImage(data: imageData) else { 304 | let error = ImageScannerControllerError.capture 305 | DispatchQueue.main.async { 306 | guard let strongSelf = self else { 307 | return 308 | } 309 | strongSelf.delegate?.captureSessionManager(strongSelf, didFailWithError: error) 310 | } 311 | return 312 | } 313 | 314 | var angle: CGFloat = 0.0 315 | 316 | switch image.imageOrientation { 317 | case .right: 318 | angle = CGFloat.pi / 2 319 | case .up: 320 | angle = CGFloat.pi 321 | default: 322 | break 323 | } 324 | 325 | var rect: Rectangle? 326 | if let displayedRectangleResult = self?.displayedRectangleResult { 327 | rect = self?.displayRectangleResult(rectangleResult: displayedRectangleResult) 328 | rect = rect?.scale(displayedRectangleResult.imageSize, image.size, withRotationAngle: angle) 329 | } 330 | 331 | DispatchQueue.main.async { 332 | guard let strongSelf = self else { 333 | return 334 | } 335 | strongSelf.delegate?.captureSessionManager(strongSelf, didCapturePicture: image, withRect: rect) 336 | } 337 | } 338 | } 339 | } 340 | 341 | /// Data structure representing the result of the detection of a rectangle. 342 | private struct RectangleDetectorResult { 343 | 344 | /// The detected rectangle. 345 | let rectangle: Rectangle 346 | 347 | /// The size of the image the rectangle was detected on. 348 | let imageSize: CGSize 349 | 350 | } 351 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Scan/FocusRectangleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FocusRectangleView.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A yellow rectangle used to display the last 'tap to focus' point 30 | final class FocusRectangleView: UIView { 31 | convenience init(touchPoint: CGPoint) { 32 | let originalSize: CGFloat = 200 33 | let finalSize: CGFloat = 80 34 | 35 | // Here, we create the frame to be the `originalSize`, with it's center being the `touchPoint`. 36 | self.init(frame: CGRect(x: touchPoint.x - (originalSize / 2), y: touchPoint.y - (originalSize / 2), width: originalSize, height: originalSize)) 37 | 38 | backgroundColor = .clear 39 | layer.borderWidth = 2.0 40 | layer.cornerRadius = 6.0 41 | layer.borderColor = UIColor.yellow.cgColor 42 | 43 | // Here, we animate the rectangle from the `originalSize` to the `finalSize` by calculating the difference. 44 | UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { 45 | self.frame.origin.x += (originalSize - finalSize) / 2 46 | self.frame.origin.y += (originalSize - finalSize) / 2 47 | 48 | self.frame.size.width -= (originalSize - finalSize) 49 | self.frame.size.height -= (originalSize - finalSize) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Scan/RectangleFeaturesFunnel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleFeaturesFunnel.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | enum AddResult { 31 | case showAndAutoScan 32 | case showOnly 33 | } 34 | 35 | /// `RectangleFeaturesFunnel` is used to improve the confidence of the detected rectangles. 36 | /// Feed rectangles to a `RectangleFeaturesFunnel` instance, and it will call the completion block with a rectangle whose confidence is high enough to be displayed. 37 | final class RectangleFeaturesFunnel { 38 | 39 | /// `RectangleMatch` is a class used to assign matching scores to rectangles. 40 | private final class RectangleMatch: NSObject { 41 | /// The rectangle feature object associated to this `RectangleMatch` instance. 42 | let rectangleFeature: Rectangle 43 | 44 | /// The score to indicate how strongly the rectangle of this instance matches other recently added rectangles. 45 | /// A higher score indicates that many recently added rectangles are very close to the rectangle of this instance. 46 | var matchingScore = 0 47 | 48 | init(rectangleFeature: Rectangle) { 49 | self.rectangleFeature = rectangleFeature 50 | } 51 | 52 | override var description: String { 53 | return "Matching score: \(matchingScore) - Rectangle: \(rectangleFeature)" 54 | } 55 | 56 | /// Whether the rectangle of this instance is within the distance of the given rectangle. 57 | /// 58 | /// - Parameters: 59 | /// - rectangle: The rectangle to compare the rectangle of this instance with. 60 | /// - threshold: The distance used to determinate if the rectangles match in pixels. 61 | /// - Returns: True if both rectangles are within the given distance of each other. 62 | func matches(_ rectangle: Rectangle, withThreshold threshold: CGFloat) -> Bool { 63 | return rectangleFeature.isWithin(threshold, ofRectangleFeature: rectangle) 64 | } 65 | } 66 | 67 | /// The queue of last added rectangles. The first rectangle is oldest one, and the last rectangle is the most recently added one. 68 | private var rectangles = [RectangleMatch]() 69 | 70 | /// The maximum number of rectangles to compare newly added rectangles with. Determines the maximum size of `rectangles`. Increasing this value will impact performance. 71 | let maxNumberOfRectangles = 8 72 | 73 | /// The minimum number of rectangles needed to start making comparaisons and determining which rectangle to display. This value should always be inferior than `maxNumberOfRectangles`. 74 | /// A higher value will delay the first time a rectangle is displayed. 75 | let minNumberOfRectangles = 3 76 | 77 | /// The value in pixels used to determine if two rectangle match or not. A higher value will prevent displayed rectangles to be refreshed. On the opposite, a smaller value will make new rectangles be displayed constantly. 78 | let matchingThreshold: CGFloat = 40.0 79 | 80 | /// The minumum number of matching rectangles (within the `rectangle` queue), to be confident enough to display a rectangle. 81 | let minNumberOfMatches = 3 82 | 83 | /// The number of similar rectangles that need to be found to auto scan. 84 | let autoScanThreshold = 35 85 | 86 | /// The number of times the rectangle has passed the threshold to be auto-scanned 87 | var currentAutoScanPassCount = 0 88 | 89 | /// The value in pixels used to determine if a rectangle is accurate enough to be auto scanned. 90 | /// A higher value means the auto scan is quicker, but the rectangle will be less accurate. On the other hand, the lower the value, the longer it'll take for the auto scan, but it'll be way more accurate 91 | var autoScanMatchingThreshold: CGFloat = 6.0 92 | 93 | /// Add a rectangle to the funnel, and if a new rectangle should be displayed, the completion block will be called. 94 | /// The algorithm works the following way: 95 | /// 1. Makes sure that the funnel has been fed enough rectangles 96 | /// 2. Removes old rectangles if needed 97 | /// 3. Compares all of the recently added rectangles to find out which one match each other 98 | /// 4. Within all of the recently added rectangles, finds the "best" one (@see `bestRectangle(withCurrentlyDisplayedRectangle:)`) 99 | /// 5. If the best rectangle is different than the currently displayed rectangle, informs the listener that a new rectangle should be displayed 100 | /// 5a. The currentAutoScanPassCount is incremented every time a new rectangle is displayed. If it passes the autoScanThreshold, we tell the listener to scan the document. 101 | /// - Parameters: 102 | /// - rectangleFeature: The rectangle to feed to the funnel. 103 | /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. 104 | /// - completion: The completion block called when a new rectangle should be displayed. 105 | func add(_ rectangleFeature: Rectangle, currentlyDisplayedRectangle currentRectangle: Rectangle?, completion: (AddResult, Rectangle) -> Void) { 106 | let rectangleMatch = RectangleMatch(rectangleFeature: rectangleFeature) 107 | rectangles.append(rectangleMatch) 108 | 109 | guard rectangles.count >= minNumberOfRectangles else { 110 | return 111 | } 112 | 113 | if rectangles.count > maxNumberOfRectangles { 114 | rectangles.removeFirst() 115 | } 116 | 117 | updateRectangleMatches() 118 | 119 | guard let bestRectangle = bestRectangle(withCurrentlyDisplayedRectangle: currentRectangle) else { 120 | return 121 | } 122 | 123 | if let previousRectangle = currentRectangle, 124 | bestRectangle.rectangleFeature.isWithin(autoScanMatchingThreshold, ofRectangleFeature: previousRectangle) { 125 | currentAutoScanPassCount += 1 126 | if currentAutoScanPassCount > autoScanThreshold { 127 | currentAutoScanPassCount = 0 128 | completion(AddResult.showAndAutoScan, bestRectangle.rectangleFeature) 129 | } 130 | } else { 131 | completion(AddResult.showOnly, bestRectangle.rectangleFeature) 132 | } 133 | } 134 | 135 | /// Determines which rectangle is best to displayed. 136 | /// The criteria used to find the best rectangle is its matching score. 137 | /// If multiple rectangles have the same matching score, we use a tie breaker to find the best rectangle (@see breakTie(forRectangles:)). 138 | /// Parameters: 139 | /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. 140 | /// Returns: The best rectangle to display given the current history. 141 | private func bestRectangle(withCurrentlyDisplayedRectangle currentRectangle: Rectangle?) -> RectangleMatch? { 142 | var bestMatch: RectangleMatch? 143 | guard !rectangles.isEmpty else { return nil } 144 | rectangles.reversed().forEach { (rectangle) in 145 | guard let best = bestMatch else { 146 | bestMatch = rectangle 147 | return 148 | } 149 | 150 | if rectangle.matchingScore > best.matchingScore { 151 | bestMatch = rectangle 152 | return 153 | } else if rectangle.matchingScore == best.matchingScore { 154 | guard let currentRectangle = currentRectangle else { 155 | return 156 | } 157 | 158 | bestMatch = breakTie(between: best, rect2: rectangle, currentRectangle: currentRectangle) 159 | } 160 | } 161 | 162 | return bestMatch 163 | } 164 | 165 | /// Breaks a tie between two rectangles to find out which is best to display. 166 | /// The first passed rectangle is returned if no other criteria could be used to break the tie. 167 | /// If the first passed rectangle (rect1) is close to the currently displayed rectangle, we pick it. 168 | /// Otherwise if the second passed rectangle (rect2) is close to the currently displayed rectangle, we pick this one. 169 | /// Finally, if none of the passed in rectangles are close to the currently displayed rectangle, we arbitrary pick the first one. 170 | /// - Parameters: 171 | /// - rect1: The first rectangle to compare. 172 | /// - rect2: The second rectangle to compare. 173 | /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. 174 | /// - Returns: The best rectangle to display between two rectangles with the same matching score. 175 | private func breakTie(between rect1: RectangleMatch, rect2: RectangleMatch, currentRectangle: Rectangle) -> RectangleMatch { 176 | if rect1.rectangleFeature.isWithin(matchingThreshold, ofRectangleFeature: currentRectangle) { 177 | return rect1 178 | } else if rect2.rectangleFeature.isWithin(matchingThreshold, ofRectangleFeature: currentRectangle) { 179 | return rect2 180 | } 181 | 182 | return rect1 183 | } 184 | 185 | /// Loops through all of the rectangles of the queue, and gives them a score depending on how many they match. @see `RectangleMatch.matchingScore` 186 | private func updateRectangleMatches() { 187 | resetMatchingScores() 188 | guard !rectangles.isEmpty else { return } 189 | for (i, currentRect) in rectangles.enumerated() { 190 | for (j, rect) in rectangles.enumerated() { 191 | if j > i && currentRect.matches(rect.rectangleFeature, withThreshold: matchingThreshold) { 192 | currentRect.matchingScore += 1 193 | rect.matchingScore += 1 194 | } 195 | } 196 | } 197 | } 198 | 199 | /// Resets the matching score of all of the rectangles in the queue to 0 200 | private func resetMatchingScores() { 201 | guard !rectangles.isEmpty else { return } 202 | for rectangle in rectangles { 203 | rectangle.matchingScore = 1 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Scan/ShutterButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShutterButton.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// A simple button used for the shutter. 30 | final class ShutterButton: UIControl { 31 | 32 | private let outterRingLayer = CAShapeLayer() 33 | private let innerCircleLayer = CAShapeLayer() 34 | 35 | private let outterRingRatio: CGFloat = 0.80 36 | private let innerRingRatio: CGFloat = 0.75 37 | 38 | private let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) 39 | 40 | override var isHighlighted: Bool { 41 | didSet { 42 | if oldValue != isHighlighted { 43 | animateInnerCircleLayer(forHighlightedState: isHighlighted) 44 | } 45 | } 46 | } 47 | 48 | // MARL: Life Cycle 49 | 50 | override init(frame: CGRect) { 51 | super.init(frame: frame) 52 | layer.addSublayer(outterRingLayer) 53 | layer.addSublayer(innerCircleLayer) 54 | backgroundColor = .clear 55 | isAccessibilityElement = true 56 | accessibilityTraits = UIAccessibilityTraits.button 57 | impactFeedbackGenerator.prepare() 58 | } 59 | 60 | required init?(coder aDecoder: NSCoder) { 61 | fatalError("init(coder:) has not been implemented") 62 | } 63 | 64 | // MARK: - Drawing 65 | 66 | override func draw(_ rect: CGRect) { 67 | super.draw(rect) 68 | 69 | outterRingLayer.frame = rect 70 | outterRingLayer.path = pathForOutterRing(inRect: rect).cgPath 71 | outterRingLayer.fillColor = UIColor.white.cgColor 72 | outterRingLayer.rasterizationScale = UIScreen.main.scale 73 | outterRingLayer.shouldRasterize = true 74 | 75 | innerCircleLayer.frame = rect 76 | innerCircleLayer.path = pathForInnerCircle(inRect: rect).cgPath 77 | innerCircleLayer.fillColor = UIColor.white.cgColor 78 | innerCircleLayer.rasterizationScale = UIScreen.main.scale 79 | innerCircleLayer.shouldRasterize = true 80 | } 81 | 82 | // MARK: - Animation 83 | 84 | private func animateInnerCircleLayer(forHighlightedState isHighlighted: Bool) { 85 | let animation = CAKeyframeAnimation(keyPath: "transform") 86 | var values = [CATransform3DMakeScale(1.0, 1.0, 1.0), CATransform3DMakeScale(0.9, 0.9, 0.9), CATransform3DMakeScale(0.93, 0.93, 0.93), CATransform3DMakeScale(0.9, 0.9, 0.9)] 87 | if isHighlighted == false { 88 | values = [CATransform3DMakeScale(0.9, 0.9, 0.9), CATransform3DMakeScale(1.0, 1.0, 1.0)] 89 | } 90 | animation.values = values 91 | animation.isRemovedOnCompletion = false 92 | animation.fillMode = CAMediaTimingFillMode.forwards 93 | animation.duration = isHighlighted ? 0.35 : 0.10 94 | 95 | innerCircleLayer.add(animation, forKey: "transform") 96 | impactFeedbackGenerator.impactOccurred() 97 | } 98 | 99 | // MARK: - Paths 100 | 101 | private func pathForOutterRing(inRect rect: CGRect) -> UIBezierPath { 102 | let path = UIBezierPath(ovalIn: rect) 103 | 104 | let innerRect = rect.scaleAndCenter(withRatio: outterRingRatio) 105 | let innerPath = UIBezierPath(ovalIn: innerRect).reversing() 106 | 107 | path.append(innerPath) 108 | 109 | return path 110 | } 111 | 112 | private func pathForInnerCircle(inRect rect: CGRect) -> UIBezierPath { 113 | let rect = rect.scaleAndCenter(withRatio: innerRingRatio) 114 | let path = UIBezierPath(ovalIn: rect) 115 | 116 | return path 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Session/CaptureSession+Focus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureSession+Focus.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | 29 | /// Extension to CaptureSession that controls auto focus 30 | extension CaptureSession { 31 | /// Sets the camera's exposure and focus point to the given point 32 | func setFocusPointToTapPoint(_ tapPoint: CGPoint) throws { 33 | guard let device = device else { 34 | let error = ImageScannerControllerError.inputDevice 35 | throw error 36 | } 37 | 38 | try device.lockForConfiguration() 39 | 40 | defer { 41 | device.unlockForConfiguration() 42 | } 43 | 44 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) { 45 | device.focusPointOfInterest = tapPoint 46 | device.focusMode = .autoFocus 47 | } 48 | 49 | if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) { 50 | device.exposurePointOfInterest = tapPoint 51 | device.exposureMode = .continuousAutoExposure 52 | } 53 | } 54 | 55 | /// Resets the camera's exposure and focus point to automatic 56 | func resetFocusToAuto() throws { 57 | guard let device = device else { 58 | let error = ImageScannerControllerError.inputDevice 59 | throw error 60 | } 61 | 62 | try device.lockForConfiguration() 63 | 64 | defer { 65 | device.unlockForConfiguration() 66 | } 67 | 68 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) { 69 | device.focusMode = .continuousAutoFocus 70 | } 71 | 72 | if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) { 73 | device.exposureMode = .continuousAutoExposure 74 | } 75 | } 76 | 77 | /// Removes an existing focus rectangle if one exists, optionally animating the exit 78 | func removeFocusRectangleIfNeeded(_ focusRectangle: FocusRectangleView?, animated: Bool) { 79 | guard let focusRectangle = focusRectangle else { return } 80 | if animated { 81 | UIView.animate(withDuration: 0.3, delay: 1.0, animations: { 82 | focusRectangle.alpha = 0.0 83 | }, completion: { (_) in 84 | focusRectangle.removeFromSuperview() 85 | }) 86 | } else { 87 | focusRectangle.removeFromSuperview() 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Session/CaptureSession+Orientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureSession+Orientation.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import CoreMotion 28 | import Foundation 29 | 30 | /// Extension to CaptureSession with support for automatically detecting the current orientation via CoreMotion 31 | /// Which works even if the user has enabled portrait lock. 32 | extension CaptureSession { 33 | /// Detect the current orientation of the device with CoreMotion and use it to set the `editImageOrientation`. 34 | func setImageOrientation() { 35 | let motion = CMMotionManager() 36 | 37 | /// This value should be 0.2, but since we only need one cycle (and stop updates immediately), 38 | /// we set it low to get the orientation immediately 39 | motion.accelerometerUpdateInterval = 0.01 40 | 41 | guard motion.isAccelerometerAvailable else { return } 42 | 43 | motion.startAccelerometerUpdates(to: OperationQueue()) { data, error in 44 | guard let data = data, error == nil else { return } 45 | 46 | /// The minimum amount of sensitivity for the landscape orientations 47 | /// This is to prevent the landscape orientation being incorrectly used 48 | /// Higher = easier for landscape to be detected, lower = easier for portrait to be detected 49 | let motionThreshold = 0.35 50 | 51 | if data.acceleration.x >= motionThreshold { 52 | self.editImageOrientation = .left 53 | } else if data.acceleration.x <= -motionThreshold { 54 | self.editImageOrientation = .right 55 | } else { 56 | /// This means the device is either in the 'up' or 'down' orientation, BUT, 57 | /// it's very rare for someone to be using their phone upside down, so we use 'up' all the time 58 | /// Which prevents accidentally making the document be scanned upside down 59 | self.editImageOrientation = .up 60 | } 61 | 62 | motion.stopAccelerometerUpdates() 63 | 64 | // If the device is reporting a specific landscape orientation, we'll use it over the accelerometer's update. 65 | // We don't use this to check for "portrait" because only the accelerometer works when portrait lock is enabled. 66 | // For some reason, the left/right orientations are incorrect (flipped) :/ 67 | switch UIDevice.current.orientation { 68 | case .landscapeLeft: 69 | self.editImageOrientation = .right 70 | case .landscapeRight: 71 | self.editImageOrientation = .left 72 | default: 73 | break 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/Session/CaptureSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureSession.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | /// A class containing global variables and settings for this capture session 31 | final class CaptureSession { 32 | 33 | static let current = CaptureSession() 34 | 35 | /// The AVCaptureDevice used for the flash and focus setting 36 | var device: CaptureDevice? 37 | 38 | /// Whether the user is past the scanning screen or not (needed to disable auto scan on other screens) 39 | var isEditing: Bool 40 | 41 | /// The status of auto scan. Auto scan tries to automatically scan a detected rectangle if it has a high enough accuracy. 42 | var isAutoScanEnabled: Bool 43 | 44 | /// The orientation of the captured image 45 | var editImageOrientation: CGImagePropertyOrientation 46 | 47 | /// The type of document to scan 48 | var isScanningTwoFacedDocument: Bool 49 | 50 | /// Property for storing results in case of 2 faced documents 51 | var firstScanResult: ImageScannerResults? 52 | 53 | private init(isAutoScanEnabled: Bool = true, editImageOrientation: CGImagePropertyOrientation = .up) { 54 | self.device = AVCaptureDevice.default(for: .video) 55 | 56 | self.isScanningTwoFacedDocument = false 57 | self.isEditing = false 58 | self.isAutoScanEnabled = isAutoScanEnabled 59 | self.editImageOrientation = editImageOrientation 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/ViewControllers/EditScanViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditScanViewController.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import AVFoundation 29 | 30 | /// The `EditScanViewController` offers an interface for the user to edit the detected rectangle. 31 | final class EditScanViewController: UIViewController, UIAdaptivePresentationControllerDelegate { 32 | 33 | lazy private var imageView: UIImageView = { 34 | let imageView = UIImageView() 35 | imageView.clipsToBounds = true 36 | imageView.isOpaque = true 37 | imageView.image = image 38 | imageView.backgroundColor = .black 39 | imageView.contentMode = .scaleAspectFit 40 | imageView.translatesAutoresizingMaskIntoConstraints = false 41 | return imageView 42 | }() 43 | 44 | lazy private var rectView: RectangleView = { 45 | let rectView = RectangleView() 46 | rectView.editable = true 47 | rectView.translatesAutoresizingMaskIntoConstraints = false 48 | return rectView 49 | }() 50 | 51 | lazy private var nextButton: UIBarButtonItem = { 52 | let title = NSLocalizedString("mbdoccapture.next_button", tableName: nil, bundle: bundle(), value: "Next", comment: "") 53 | let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(pushReviewController)) 54 | button.tintColor = .white 55 | return button 56 | }() 57 | 58 | lazy private var cancelButton: UIBarButtonItem = { 59 | let title = NSLocalizedString("mbdoccapture.cancel_button", tableName: nil, bundle: bundle(), value: "Cancel", comment: "") 60 | let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(dismissEditScanViewControllerController)) 61 | button.tintColor = .white 62 | return button 63 | }() 64 | 65 | /// The image the rectangle was detected on. 66 | private let image: UIImage 67 | 68 | /// The detected rectangle that can be edited by the user. Uses the image's coordinates. 69 | private var rect: Rectangle 70 | 71 | private var zoomGestureController: ZoomGestureController! 72 | 73 | private var rectViewWidthConstraint = NSLayoutConstraint() 74 | private var rectViewHeightConstraint = NSLayoutConstraint() 75 | 76 | // MARK: - Life Cycle 77 | 78 | init(image: UIImage, rect: Rectangle?, rotateImage: Bool = true) { 79 | self.image = rotateImage ? image.applyingPortraitOrientation() : image 80 | self.rect = rect ?? EditScanViewController.defaultRectangle(forImage: image) 81 | super.init(nibName: nil, bundle: nil) 82 | } 83 | 84 | required init?(coder aDecoder: NSCoder) { 85 | fatalError("init(coder:) has not been implemented") 86 | } 87 | 88 | override func viewDidLoad() { 89 | super.viewDidLoad() 90 | 91 | setupViews() 92 | setupConstraints() 93 | title = NSLocalizedString("mbdoccapture.scan_edit_title", tableName: nil, bundle: bundle(), value: "Trimming", comment: "") 94 | navigationItem.rightBarButtonItem = nextButton 95 | navigationItem.leftBarButtonItem = cancelButton 96 | 97 | if #available(iOS 13.0, *) { 98 | isModalInPresentation = false 99 | navigationController?.presentationController?.delegate = self 100 | } 101 | 102 | zoomGestureController = ZoomGestureController(image: image, rectView: rectView) 103 | 104 | let touchDown = UILongPressGestureRecognizer(target: zoomGestureController, action: #selector(zoomGestureController.handle(pan:))) 105 | touchDown.minimumPressDuration = 0 106 | view.addGestureRecognizer(touchDown) 107 | } 108 | 109 | override func viewDidLayoutSubviews() { 110 | super.viewDidLayoutSubviews() 111 | adjustRectViewConstraints() 112 | displayRect() 113 | } 114 | 115 | override func viewWillDisappear(_ animated: Bool) { 116 | super.viewWillDisappear(animated) 117 | 118 | // Work around for an iOS 11.2 bug where UIBarButtonItems don't get back to their normal state after being pressed. 119 | navigationController?.navigationBar.tintAdjustmentMode = .normal 120 | navigationController?.navigationBar.tintAdjustmentMode = .automatic 121 | } 122 | 123 | // MARK: - Setups 124 | 125 | private func setupViews() { 126 | view.addSubview(imageView) 127 | view.addSubview(rectView) 128 | } 129 | 130 | private func setupConstraints() { 131 | let imageViewConstraints = [ 132 | imageView.topAnchor.constraint(equalTo: view.topAnchor), 133 | imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 134 | view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), 135 | view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor) 136 | ] 137 | 138 | rectViewWidthConstraint = rectView.widthAnchor.constraint(equalToConstant: 0.0) 139 | rectViewHeightConstraint = rectView.heightAnchor.constraint(equalToConstant: 0.0) 140 | 141 | let rectViewConstraints = [ 142 | rectView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 143 | rectView.centerYAnchor.constraint(equalTo: view.centerYAnchor), 144 | rectViewWidthConstraint, 145 | rectViewHeightConstraint 146 | ] 147 | 148 | NSLayoutConstraint.activate(rectViewConstraints + imageViewConstraints) 149 | } 150 | 151 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 152 | return false 153 | } 154 | 155 | // MARK: - Actions 156 | 157 | @objc func dismissEditScanViewControllerController() { 158 | dismiss(animated: true) 159 | } 160 | @objc func pushReviewController() { 161 | guard let rect = rectView.rect, 162 | let ciImage = CIImage(image: image) else { 163 | if let imageScannerController = navigationController as? ImageScannerController { 164 | let error = ImageScannerControllerError.ciImageCreation 165 | imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFailWithError: error) 166 | } 167 | return 168 | } 169 | 170 | let scaledRect = rect.scale(rectView.bounds.size, image.size) 171 | self.rect = scaledRect 172 | 173 | var cartesianScaledRect = scaledRect.toCartesian(withHeight: image.size.height) 174 | cartesianScaledRect.reorganize() 175 | 176 | let filteredImage = ciImage.applyingFilter("CIPerspectiveCorrection", parameters: [ 177 | "inputTopLeft": CIVector(cgPoint: cartesianScaledRect.bottomLeft), 178 | "inputTopRight": CIVector(cgPoint: cartesianScaledRect.bottomRight), 179 | "inputBottomLeft": CIVector(cgPoint: cartesianScaledRect.topLeft), 180 | "inputBottomRight": CIVector(cgPoint: cartesianScaledRect.topRight) 181 | ]) 182 | 183 | let enhancedImage = filteredImage.applyingAdaptiveThreshold()?.withFixedOrientation() 184 | 185 | var uiImage: UIImage! 186 | 187 | // Let's try to generate the CGImage from the CIImage before creating a UIImage. 188 | if let cgImage = CIContext(options: nil).createCGImage(filteredImage, from: filteredImage.extent) { 189 | uiImage = UIImage(cgImage: cgImage) 190 | } else { 191 | uiImage = UIImage(ciImage: filteredImage, scale: 1.0, orientation: .up) 192 | } 193 | 194 | let finalImage = uiImage.withFixedOrientation() 195 | 196 | let results = ImageScannerResults(originalImage: image, scannedImage: finalImage, enhancedImage: enhancedImage, doesUserPreferEnhancedImage: false, detectedRectangle: scaledRect) 197 | let reviewViewController = ReviewViewController(results: results) 198 | 199 | navigationController?.pushViewController(reviewViewController, animated: true) 200 | } 201 | 202 | private func displayRect() { 203 | let imageSize = image.size 204 | let imageFrame = CGRect(origin: rectView.frame.origin, size: CGSize(width: rectViewWidthConstraint.constant, height: rectViewHeightConstraint.constant)) 205 | 206 | let scaleTransform = CGAffineTransform.scaleTransform(forSize: imageSize, aspectFillInSize: imageFrame.size) 207 | let transforms = [scaleTransform] 208 | let transformedRect = rect.applyTransforms(transforms) 209 | 210 | rectView.drawRectangle(rect: transformedRect, animated: false) 211 | } 212 | 213 | /// The rectView should be lined up on top of the actual image displayed by the imageView. 214 | /// Since there is no way to know the size of that image before run time, we adjust the constraints to make sure that the rectView is on top of the displayed image. 215 | private func adjustRectViewConstraints() { 216 | let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds) 217 | rectViewWidthConstraint.constant = frame.size.width 218 | rectViewHeightConstraint.constant = frame.size.height 219 | } 220 | 221 | /// Generates a `Rectangle` object that's centered and one third of the size of the passed in image. 222 | private static func defaultRectangle(forImage image: UIImage) -> Rectangle { 223 | let topLeft = CGPoint(x: image.size.width / 3.0, y: image.size.height / 3.0) 224 | let topRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: image.size.height / 3.0) 225 | let bottomRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) 226 | let bottomLeft = CGPoint(x: image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) 227 | 228 | let rect = Rectangle(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) 229 | 230 | return rect 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/ViewControllers/ImageScannerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageScannerController.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | import AVFoundation 29 | 30 | /// A set of methods that your delegate object must implement to interact with the image scanner interface. 31 | public protocol ImageScannerControllerDelegate: NSObjectProtocol { 32 | 33 | /// Tells the delegate that the user scanned a document. 34 | /// 35 | /// - Parameters: 36 | /// - scanner: The scanner controller object managing the scanning interface. 37 | /// - results: The results of the user scanning with the camera. 38 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 39 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) 40 | 41 | /// Tells the delegate that the user scanned a document. 42 | /// 43 | /// - Parameters: 44 | /// - scanner: The scanner controller object managing the scanning interface. 45 | /// - page1Results: The results of the user scanning page 1. 46 | /// - page2Results: The results of the user scanning page 2. 47 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 48 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithPage1Results page1Results: ImageScannerResults, andPage2Results page2Results: ImageScannerResults) 49 | 50 | /// Tells the delegate that the user cancelled the scan operation. 51 | /// 52 | /// - Parameters: 53 | /// - scanner: The scanner controller object managing the scanning interface. 54 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 55 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController) 56 | 57 | /// Tells the delegate that an error occured during the user's scanning experience. 58 | /// 59 | /// - Parameters: 60 | /// - scanner: The scanner controller object managing the scanning interface. 61 | /// - error: The error that occured. 62 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) 63 | } 64 | 65 | /// A view controller that manages the full flow for scanning documents. 66 | /// The `ImageScannerController` class is meant to be presented. It consists of a series of 3 different screens which guide the user: 67 | /// 1. Uses the camera to capture an image with a rectangle that has been detected. 68 | /// 2. Edit the detected rectangle. 69 | /// 3. Review the cropped down version of the rectangle. 70 | public final class ImageScannerController: UINavigationController { 71 | 72 | /// The object that acts as the delegate of the `ImageScannerController`. 73 | weak public var imageScannerDelegate: ImageScannerControllerDelegate? 74 | 75 | public var shouldScanTwoFaces: Bool = false { 76 | didSet { 77 | CaptureSession.current.isScanningTwoFacedDocument = shouldScanTwoFaces 78 | } 79 | } 80 | 81 | // MARK: - Life Cycle 82 | 83 | /// A black UIView, used to quickly display a black screen when the shutter button is presseed. 84 | internal let blackFlashView: UIView = { 85 | let view = UIView() 86 | view.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 87 | view.isHidden = true 88 | view.translatesAutoresizingMaskIntoConstraints = false 89 | return view 90 | }() 91 | 92 | public required init(image: UIImage? = nil, delegate: ImageScannerControllerDelegate? = nil) { 93 | super.init(rootViewController: ScannerViewController()) 94 | 95 | self.imageScannerDelegate = delegate 96 | 97 | navigationBar.tintColor = .white 98 | self.view.addSubview(blackFlashView) 99 | setupConstraints() 100 | 101 | // If an image was passed in by the host app (e.g. picked from the photo library), use it instead of the document scanner. 102 | if let image = image { 103 | 104 | var detectedRect: Rectangle? 105 | 106 | // Whether or not we detect a rect, present the edit view controller after attempting to detect a rect. 107 | defer { 108 | let editViewController = EditScanViewController(image: image, rect: detectedRect, rotateImage: false) 109 | setViewControllers([editViewController], animated: false) 110 | } 111 | 112 | guard let ciImage = CIImage(image: image) else { return } 113 | 114 | detectedRect = CIRectangleDetector.rectangle(forImage: ciImage) 115 | detectedRect?.reorganize() 116 | } 117 | } 118 | 119 | public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 120 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 121 | } 122 | 123 | required public init?(coder aDecoder: NSCoder) { 124 | fatalError("init(coder:) has not been implemented") 125 | } 126 | 127 | private func setupConstraints() { 128 | let blackFlashViewConstraints = [ 129 | blackFlashView.topAnchor.constraint(equalTo: view.topAnchor), 130 | blackFlashView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 131 | view.bottomAnchor.constraint(equalTo: blackFlashView.bottomAnchor), 132 | view.trailingAnchor.constraint(equalTo: blackFlashView.trailingAnchor) 133 | ] 134 | 135 | NSLayoutConstraint.activate(blackFlashViewConstraints) 136 | } 137 | 138 | override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { 139 | return .portrait 140 | } 141 | 142 | override public var shouldAutorotate: Bool { 143 | return true 144 | } 145 | 146 | override public var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { 147 | return .portrait 148 | } 149 | 150 | internal func flashToBlack() { 151 | view.bringSubviewToFront(blackFlashView) 152 | blackFlashView.isHidden = false 153 | let flashDuration = DispatchTime.now() + 0.05 154 | DispatchQueue.main.asyncAfter(deadline: flashDuration) { 155 | self.blackFlashView.isHidden = true 156 | } 157 | } 158 | } 159 | 160 | /// Data structure containing information about a scan. 161 | public struct ImageScannerResults { 162 | 163 | /// The original image taken by the user, prior to the cropping applied. 164 | public var originalImage: UIImage 165 | 166 | /// The deskewed and cropped orignal image using the detected rectangle, without any filters. 167 | public var scannedImage: UIImage 168 | 169 | /// The enhanced image, passed through an Adaptive Thresholding function. This image will always be grayscale and may not always be available. 170 | public var enhancedImage: UIImage? 171 | 172 | /// Whether the user wants to use the enhanced image or not. The `enhancedImage`, for use with OCR or similar uses, may still be available even if it has not been selected by the user. 173 | public var doesUserPreferEnhancedImage: Bool 174 | 175 | /// The detected rectangle which was used to generate the `scannedImage`. 176 | public var detectedRectangle: Rectangle 177 | 178 | } 179 | 180 | extension UIViewController { 181 | 182 | func bundle() -> Bundle { 183 | let frameworkBundle = Bundle(for: type(of: self)) 184 | let bundleURL = frameworkBundle.resourceURL?.appendingPathComponent("MBDocCapture.bundle") 185 | 186 | if let bundle = Bundle(url: bundleURL!) { 187 | return bundle 188 | } else { 189 | return Bundle(for: type(of: self)) 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/ViewControllers/ReviewViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReviewViewController.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import UIKit 28 | 29 | /// The `ReviewViewController` offers an interface to review the image after it has been cropped and deskwed according to the passed in rectangle. 30 | final class ReviewViewController: UIViewController, UIAdaptivePresentationControllerDelegate { 31 | 32 | private var rotationAngle = Measurement(value: 0, unit: .degrees) 33 | private var enhancedImageIsAvailable = false 34 | private var isCurrentlyDisplayingEnhancedImage = false 35 | 36 | lazy var imageView: UIImageView = { 37 | let imageView = UIImageView() 38 | imageView.clipsToBounds = true 39 | imageView.isOpaque = true 40 | imageView.image = results.scannedImage 41 | imageView.backgroundColor = .black 42 | imageView.contentMode = .scaleAspectFit 43 | imageView.translatesAutoresizingMaskIntoConstraints = false 44 | return imageView 45 | }() 46 | 47 | lazy private var enhanceButton: UIBarButtonItem = { 48 | let image = UIImage(named: "enhance", in: bundle(), compatibleWith: nil) 49 | let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(toggleEnhancedImage)) 50 | button.tintColor = .white 51 | return button 52 | }() 53 | 54 | lazy private var rotateButton: UIBarButtonItem = { 55 | let image = UIImage(named: "rotate", in: bundle(), compatibleWith: nil) 56 | let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(rotateImage)) 57 | button.tintColor = .white 58 | return button 59 | }() 60 | 61 | lazy private var doneButton: UIBarButtonItem = { 62 | let title = NSLocalizedString("mbdoccapture.next_button", tableName: nil, bundle: bundle(), value: "Next", comment: "") 63 | let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(finishScan)) 64 | button.tintColor = .white 65 | return button 66 | }() 67 | 68 | private let results: ImageScannerResults 69 | 70 | // MARK: - Life Cycle 71 | 72 | init(results: ImageScannerResults) { 73 | self.results = results 74 | super.init(nibName: nil, bundle: nil) 75 | } 76 | 77 | required init?(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | override func viewDidLoad() { 82 | super.viewDidLoad() 83 | 84 | enhancedImageIsAvailable = results.enhancedImage != nil 85 | 86 | setupViews() 87 | setupToolbar() 88 | setupConstraints() 89 | 90 | if #available(iOS 13.0, *) { 91 | isModalInPresentation = false 92 | navigationController?.presentationController?.delegate = self 93 | } 94 | 95 | title = NSLocalizedString("mbdoccapture.scan_review_title", tableName: nil, bundle: bundle(), value: "Confirmation", comment: "") 96 | navigationItem.rightBarButtonItem = doneButton 97 | } 98 | 99 | override func viewWillAppear(_ animated: Bool) { 100 | super.viewWillAppear(animated) 101 | 102 | // We only show the toolbar (with the enhance button) if the enhanced image is available. 103 | if enhancedImageIsAvailable { 104 | navigationController?.setToolbarHidden(false, animated: true) 105 | } 106 | } 107 | 108 | override func viewWillDisappear(_ animated: Bool) { 109 | super.viewWillDisappear(animated) 110 | navigationController?.setToolbarHidden(true, animated: true) 111 | } 112 | 113 | func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 114 | return false 115 | } 116 | 117 | // MARK: Setups 118 | 119 | private func setupViews() { 120 | view.addSubview(imageView) 121 | } 122 | 123 | private func setupToolbar() { 124 | guard enhancedImageIsAvailable else { return } 125 | 126 | navigationController?.toolbar.barStyle = .blackTranslucent 127 | 128 | let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) 129 | let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 130 | toolbarItems = [fixedSpace, enhanceButton, flexibleSpace, rotateButton, fixedSpace] 131 | } 132 | 133 | private func setupConstraints() { 134 | let imageViewConstraints = [ 135 | imageView.topAnchor.constraint(equalTo: view.topAnchor), 136 | imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), 137 | view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), 138 | view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor) 139 | ] 140 | 141 | NSLayoutConstraint.activate(imageViewConstraints) 142 | } 143 | 144 | // MARK: - Actions 145 | 146 | @objc private func reloadImage() { 147 | if enhancedImageIsAvailable, isCurrentlyDisplayingEnhancedImage { 148 | imageView.image = results.enhancedImage?.rotated(by: rotationAngle) ?? results.enhancedImage 149 | } else { 150 | imageView.image = results.scannedImage.rotated(by: rotationAngle) ?? results.scannedImage 151 | } 152 | } 153 | 154 | @objc func toggleEnhancedImage() { 155 | guard enhancedImageIsAvailable else { return } 156 | 157 | isCurrentlyDisplayingEnhancedImage.toggle() 158 | reloadImage() 159 | 160 | if isCurrentlyDisplayingEnhancedImage { 161 | enhanceButton.tintColor = UIColor(red: 64 / 255.0, green: 159 / 255.0, blue: 255 / 255.0, alpha: 1.0) 162 | } else { 163 | enhanceButton.tintColor = .white 164 | } 165 | } 166 | 167 | @objc func rotateImage() { 168 | rotationAngle.value += 90 169 | 170 | if rotationAngle.value == 360 { 171 | rotationAngle.value = 0 172 | } 173 | 174 | reloadImage() 175 | } 176 | 177 | @objc private func finishScan() { 178 | guard let imageScannerController = navigationController as? ImageScannerController else { return } 179 | var newResults = results 180 | newResults.scannedImage = results.scannedImage.rotated(by: rotationAngle) ?? results.scannedImage 181 | newResults.enhancedImage = results.enhancedImage?.rotated(by: rotationAngle) ?? results.enhancedImage 182 | newResults.doesUserPreferEnhancedImage = isCurrentlyDisplayingEnhancedImage 183 | if CaptureSession.current.isScanningTwoFacedDocument { 184 | if let firstPageResult = CaptureSession.current.firstScanResult { 185 | imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFinishScanningWithPage1Results: firstPageResult, andPage2Results: newResults) 186 | CaptureSession.current.isScanningTwoFacedDocument = false 187 | CaptureSession.current.firstScanResult = nil 188 | } else { 189 | CaptureSession.current.firstScanResult = newResults 190 | navigationController?.popToRootViewController(animated: true) 191 | } 192 | } else { 193 | imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFinishScanningWithResults: newResults) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /MBDocCapture/Classes/ViewControllers/ZoomGestureController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZoomGestureController.swift 3 | // MBDocCapture 4 | // 5 | // Created by El Mahdi Boukhris on 16/04/2019. 6 | // Copyright © 2019 El Mahdi Boukhris 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to 10 | // deal in the Software without restriction, including without limitation the 11 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12 | // sell copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 | // DEALINGS IN THE SOFTWARE. 25 | // 26 | 27 | import Foundation 28 | import AVFoundation 29 | 30 | final class ZoomGestureController { 31 | 32 | private let image: UIImage 33 | private let rectView: RectangleView 34 | 35 | init(image: UIImage, rectView: RectangleView) { 36 | self.image = image 37 | self.rectView = rectView 38 | } 39 | 40 | private var previousPanPosition: CGPoint? 41 | private var closestCorner: CornerPosition? 42 | 43 | @objc func handle(pan: UIGestureRecognizer) { 44 | guard let drawnRect = rectView.rect else { 45 | return 46 | } 47 | 48 | guard pan.state != .ended else { 49 | self.previousPanPosition = nil 50 | self.closestCorner = nil 51 | rectView.resetHighlightedCornerViews() 52 | return 53 | } 54 | 55 | let position = pan.location(in: rectView) 56 | 57 | let previousPanPosition = self.previousPanPosition ?? position 58 | let closestCorner = self.closestCorner ?? position.closestCornerFrom(rect: drawnRect) 59 | 60 | let offset = CGAffineTransform(translationX: position.x - previousPanPosition.x, y: position.y - previousPanPosition.y) 61 | let cornerView = rectView.cornerViewForCornerPosition(position: closestCorner) 62 | let draggedCornerViewCenter = cornerView.center.applying(offset) 63 | 64 | rectView.moveCorner(cornerView: cornerView, atPoint: draggedCornerViewCenter) 65 | 66 | self.previousPanPosition = position 67 | self.closestCorner = closestCorner 68 | 69 | let scale = image.size.width / rectView.bounds.size.width 70 | let scaledDraggedCornerViewCenter = CGPoint(x: draggedCornerViewCenter.x * scale, y: draggedCornerViewCenter.y * scale) 71 | guard let zoomedImage = image.scaledImage(atPoint: scaledDraggedCornerViewCenter, scaleFactor: 2.5, targetSize: rectView.bounds.size) else { 72 | return 73 | } 74 | 75 | rectView.highlightCornerAtPosition(position: closestCorner, with: zoomedImage) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MBDocCapture 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/MBDocCapture.svg?style=flat)](https://cocoapods.org/pods/MBDocCapture) 4 | [![License](https://img.shields.io/cocoapods/l/MBDocCapture.svg?style=flat)](https://cocoapods.org/pods/MBDocCapture) 5 | [![Platform](https://img.shields.io/cocoapods/p/MBDocCapture.svg?style=flat)](https://cocoapods.org/pods/MBDocCapture) 6 | 7 | **MBDocCapture** makes it easy to add document scanning functionalities to your iOS app but also image editing (Cropping and contrast enhacement). 8 | 9 | - [Features](#features) 10 | - [Demo](#demo) 11 | - [Requirements](#requirements) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | 15 | ## Features 16 | 17 | - [x] Doc scanning 18 | - [x] Photo cropping and enhancement 19 | - [x] Auto scan 20 | 21 | ## Demo 22 | 23 |

24 | 25 |

26 | 27 | ## Requirements 28 | 29 | - Swift 4.2 30 | - iOS 10.0+ 31 | 32 |
33 | 34 | ## Installation 35 | ### Cocoapods 36 | 37 | [CocoaPods](http://cocoapods.org) is a dependency manager for Cocoa projects. 38 | 39 | To integrate **MBDocCapture** into your Xcode project using CocoaPods, specify it in your `Podfile`: 40 | 41 | ```rubygi 42 | source 'https://github.com/CocoaPods/Specs.git' 43 | platform :ios, '10.0' 44 | use_frameworks! 45 | 46 | target '' do 47 | pod 'MBDocCapture' 48 | end 49 | ``` 50 | 51 | Then, run the following command: 52 | 53 | ```bash 54 | $ pod install 55 | ``` 56 | 57 | ## Usage 58 | 59 | ### Swift 60 | 61 | 1. Make sure that your view controller conforms to the `ImageScannerControllerDelegate` protocol: 62 | 63 | ```swift 64 | class YourViewController: UIViewController, ImageScannerControllerDelegate { 65 | // YourViewController code here 66 | } 67 | ``` 68 | 69 | 2. Implement the delegate functions inside your view controller: 70 | ```swift 71 | /// Tells the delegate that the user scanned a document. 72 | /// 73 | /// - Parameters: 74 | /// - scanner: The scanner controller object managing the scanning interface. 75 | /// - results: The results of the user scanning with the camera. 76 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 77 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { 78 | scanner.dismiss() 79 | } 80 | 81 | /// Tells the delegate that the user scanned a document. 82 | /// 83 | /// - Parameters: 84 | /// - scanner: The scanner controller object managing the scanning interface. 85 | /// - page1Results: The results of the user scanning page 1. 86 | /// - page2Results: The results of the user scanning page 2. 87 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 88 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithPage1Results page1Results: ImageScannerResults, andPage2Results page2Results: ImageScannerResults) { 89 | scanner.dismiss() 90 | } 91 | 92 | /// Tells the delegate that the user cancelled the scan operation. 93 | /// 94 | /// - Parameters: 95 | /// - scanner: The scanner controller object managing the scanning interface. 96 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. 97 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { 98 | scanner.dismiss() 99 | } 100 | 101 | /// Tells the delegate that an error occured during the user's scanning experience. 102 | /// 103 | /// - Parameters: 104 | /// - scanner: The scanner controller object managing the scanning interface. 105 | /// - error: The error that occured. 106 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { 107 | scanner.dismiss() 108 | } 109 | ``` 110 | 111 | 3. Finally, create and present a `ImageScannerController` instance somewhere within your view controller: 112 | 113 | ```swift 114 | let scannerViewController = ImageScannerController(delegate: self) 115 | //scannerViewController.shouldScanTwoFaces = false // Use this to scan the front and the back of a document 116 | present(scannerViewController, animated: true) 117 | ``` 118 | 119 | ## License 120 | 121 | MBDocCapture is available under the MIT license. See the LICENSE file for more info. 122 | 123 | ## Support 124 | If this project helped you, buy me coffee :coffee: 125 | 126 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/BEMahdi) 127 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------