├── .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 | [](https://cocoapods.org/pods/MBDocCapture)
4 | [](https://cocoapods.org/pods/MBDocCapture)
5 | [](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 | [](https://paypal.me/BEMahdi)
127 |
--------------------------------------------------------------------------------
/_Pods.xcodeproj:
--------------------------------------------------------------------------------
1 | Example/Pods/Pods.xcodeproj
--------------------------------------------------------------------------------