123 | ```
124 |
125 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/)
126 | with a clear title and description.
127 |
128 | ### Conventions of commit messages
129 |
130 | Adding features on repo
131 |
132 | ```bash
133 | git commit -m "feat: message about this feature"
134 | ```
135 |
136 | Fixing features on repo
137 |
138 | ```bash
139 | git commit -m "fix: message about this update"
140 | ```
141 |
142 | Removing features on repo
143 |
144 | ```bash
145 | git commit -m "refactor: message about this" -m "BREAKING CHANGE: message about the breaking change"
146 | ```
147 |
148 |
149 | **IMPORTANT**: By submitting a patch, you agree to allow the project owner to
150 | license your work under the same license as that used by the project, which is available [here](LICENSE.md).
151 |
152 | ### Discussions
153 |
154 | We aim to keep all project discussion inside Github Issues. This is to make sure valuable discussion is accessible via search. If you have questions about how to use the library, or how the project is running - Github Issues are the goto tool for this project.
155 |
156 | #### Our expectations on you as a contributor
157 |
158 | We want contributors to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an enjoyable way. 🎉
159 |
160 | The fact that you'll have push access will allow you to:
161 |
162 | - Avoid having to fork the project if you want to submit other pull requests as you'll be able to create branches directly on the project.
163 | - Help triage issues, merge pull requests.
164 | - Pick up the project if other maintainers move their focus elsewhere.
165 |
--------------------------------------------------------------------------------
/Changelog.md:
--------------------------------------------------------------------------------
1 | ### 3.0.0
2 | - Remove Carthage and manual installation instructions ([#367](https://github.com/WeTransfer/WeScan/pull/367)) via [@BasThomas](https://github.com/BasThomas)
3 | - Merge release 3.0.0-beta.1 into master ([#366](https://github.com/WeTransfer/WeScan/pull/366)) via [@wetransferplatform](https://github.com/wetransferplatform)
4 |
5 | ### 3.0.0-beta.1
6 | - Copy snapshots to test bundle to solve SPM warning ([#363](https://github.com/WeTransfer/WeScan/pull/363)) via [@valeriyvan](https://github.com/valeriyvan)
7 | - Fix typo ([#362](https://github.com/WeTransfer/WeScan/pull/362)) via [@valeriyvan](https://github.com/valeriyvan)
8 | - Remove deprecated properties ([#360](https://github.com/WeTransfer/WeScan/pull/360)) via [@valeriyvan](https://github.com/valeriyvan)
9 | - Fix typos ([#357](https://github.com/WeTransfer/WeScan/pull/357)) via [@valeriyvan](https://github.com/valeriyvan)
10 | - Merge release 2.1.0 into master ([#352](https://github.com/WeTransfer/WeScan/pull/352)) via [@wetransferplatform](https://github.com/wetransferplatform)
11 |
12 | ### 2.1.0
13 | - Update CI module to fix CI ([#351](https://github.com/WeTransfer/WeScan/pull/351)) via [@AvdLee](https://github.com/AvdLee)
14 | - Fix resetMatchingScores ([#349](https://github.com/WeTransfer/WeScan/pull/349)) via [@lengocgiang](https://github.com/lengocgiang)
15 | - Update CODEOWNERS ([#343](https://github.com/WeTransfer/WeScan/pull/343)) via [@peagasilva](https://github.com/peagasilva)
16 | - Merge release 2.0.0 into master ([#342](https://github.com/WeTransfer/WeScan/pull/342)) via [@wetransferplatform](https://github.com/wetransferplatform)
17 |
18 | ### 2.0.0
19 | - Fixed SwiftUI Previews in Xcode >= 14 ([#338](https://github.com/WeTransfer/WeScan/pull/338)) via [@amarildolucas](https://github.com/amarildolucas)
20 | - Update CI to latest ([#339](https://github.com/WeTransfer/WeScan/pull/339)) via [@AvdLee](https://github.com/AvdLee)
21 |
22 | ### 1.8.1
23 | - Fix broken iOS 14 AV apple api ([#293](https://github.com/WeTransfer/WeScan/pull/293)) via [@ErikGro](https://github.com/ErikGro)
24 | - ! add japan language ([#281](https://github.com/WeTransfer/WeScan/pull/281)) via [@padgithub](https://github.com/padgithub)
25 | - Localization - Add support for Dutch language ([#285](https://github.com/WeTransfer/WeScan/pull/285)) via [@marvukusic](https://github.com/marvukusic)
26 | - Fix tests, update to use the iPhone 12 simulator. ([#290](https://github.com/WeTransfer/WeScan/pull/290)) via [@AvdLee](https://github.com/AvdLee)
27 | - Merge release 1.8.0 into master ([#280](https://github.com/WeTransfer/WeScan/pull/280)) via [@wetransferplatform](https://github.com/wetransferplatform)
28 |
29 | ### 1.8.0
30 | - SPM Support ([#172](https://github.com/WeTransfer/WeScan/issues/172)) via [@AvdLee](https://github.com/AvdLee)
31 | - Typo fix in the comment ([#272](https://github.com/WeTransfer/WeScan/pull/272)) via [@PermanAtayev](https://github.com/PermanAtayev)
32 | - Realign table of contents and rest of the README ([#271](https://github.com/WeTransfer/WeScan/pull/271)) via [@jacquerie](https://github.com/jacquerie)
33 | - Feat: added Arabic language support ([#267](https://github.com/WeTransfer/WeScan/pull/267)) via [@mohammadhamdan1991](https://github.com/mohammadhamdan1991)
34 | - Czech language support ([#259](https://github.com/WeTransfer/WeScan/pull/259)) via [@killalad](https://github.com/killalad)
35 | - Merge release 1.7.0 into master ([#256](https://github.com/WeTransfer/WeScan/pull/256)) via [@ghost](https://github.com/ghost)
36 |
37 | ### 1.7.0
38 | - Create individual Scanner and Review image controller ([#213](https://github.com/WeTransfer/WeScan/pull/213)) via [@chawatvish](https://github.com/chawatvish)
39 | - Merge release 1.6.0 into master ([#254](https://github.com/WeTransfer/WeScan/pull/254)) via [@WeTransferBot](https://github.com/WeTransferBot)
40 |
41 | ### 1.6.0
42 | - Allow support for using an image after instantiation ([#251](https://github.com/WeTransfer/WeScan/pull/251)) via [@erikvillegas](https://github.com/erikvillegas)
43 | - SF Symbols ([#250](https://github.com/WeTransfer/WeScan/pull/250)) via [@andschdk](https://github.com/andschdk)
44 | - Use same yellow tint color. ([#249](https://github.com/WeTransfer/WeScan/pull/249)) via [@andschdk](https://github.com/andschdk)
45 | - Update cancel button title, wescan.edit.button.cancel is not found ([#246](https://github.com/WeTransfer/WeScan/pull/246)) via [@thomasdao](https://github.com/thomasdao)
46 | - Fix error description typo ([#244](https://github.com/WeTransfer/WeScan/pull/244)) via [@danilovmaxim](https://github.com/danilovmaxim)
47 | - Update EditScanViewController.swift ([#242](https://github.com/WeTransfer/WeScan/pull/242)) via [@hakan-codeway](https://github.com/hakan-codeway)
48 | - Update Localizable.strings ([#239](https://github.com/WeTransfer/WeScan/pull/239)) via [@hakan-codeway](https://github.com/hakan-codeway)
49 | - Update ReviewViewController.swift ([#240](https://github.com/WeTransfer/WeScan/pull/240)) via [@hakan-codeway](https://github.com/hakan-codeway)
50 | - Safe assignment of the `AVCaptureSession` preset value. ([#238](https://github.com/WeTransfer/WeScan/pull/238)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck)
51 | - Added delegate argument to class `CaptureSessionManager` init. ([#237](https://github.com/WeTransfer/WeScan/pull/237)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck)
52 | - Trivial marker typo fix. ([#234](https://github.com/WeTransfer/WeScan/pull/234)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck)
53 | - [localization] add Russian language support ([#235](https://github.com/WeTransfer/WeScan/pull/235)) via [@DmitriyTor](https://github.com/DmitriyTor)
54 | - 🇹🇷 Turkish localization added ([#231](https://github.com/WeTransfer/WeScan/pull/231)) via [@Adem68](https://github.com/Adem68)
55 | - Re-scale height independently in Quadrilateral CGAffineTransform ([#228](https://github.com/WeTransfer/WeScan/pull/228)) via [@winsonluk](https://github.com/winsonluk)
56 | - Merge release 1.5.0 into master ([#225](https://github.com/WeTransfer/WeScan/pull/225)) via [@WeTransferBot](https://github.com/WeTransferBot)
57 |
58 | ### 1.5.0
59 | - Update xcode project - include polish translation - improvements ([#224](https://github.com/WeTransfer/WeScan/pull/224)) via @lukszar
60 | - Create polish localization ([#221](https://github.com/WeTransfer/WeScan/pull/221)) via @lukszar
61 | - ES and LATAM spanish translations ([#223](https://github.com/WeTransfer/WeScan/pull/223)) via @nicoonguitar
62 | - Updated the readme to avoid some small initial configuration issues. ([#222](https://github.com/WeTransfer/WeScan/pull/222)) via @Ovid-iu
63 | - Merge release 1.4.0 into master ([#212](https://github.com/WeTransfer/WeScan/pull/212)) via @WeTransferBot
64 |
65 | ### 1.4.0
66 |
67 | - Migrate to Bitrise & Danger-Swift ([#211](https://github.com/WeTransfer/WeScan/pull/211)) via @AvdLee
68 |
69 | ### 1.3.0
70 | - Updated SwiftLint code style rules
71 | - Forcing a changelog entry now from CI
72 |
73 | ### 1.1.0
74 |
75 | - Updated to Swift 5.0
76 | - Added German translation
77 | - Several small improvements
78 |
79 | ### 1.0 (2019-01-08)
80 |
81 | - Add support for French, Italian, Portuguese, Chinese (Simplified) and Chinese (Traditional)
82 | - Add support to enhance the scanned image using AdaptiveThresholding
83 | - Updated to Swift 4.2
84 | - Add auto rotate, auto scan, and Vision support
--------------------------------------------------------------------------------
/Example/WeScan.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/WeScan.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/WeScan.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-snapshot-testing",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
7 | "state" : {
8 | "revision" : "f29e2014f6230cf7d5138fc899da51c7f513d467",
9 | "version" : "1.10.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // WeScanSampleProject
4 | //
5 | // Created by Boris Emorine on 2/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | final class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 | func application(
17 | _ application: UIApplication,
18 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
19 | ) -> Bool {
20 | return true
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "WeScanAppIcon20pt@2x.jpg",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "WeScanAppIcon20pt@3x.jpg",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "WeScanAppIcon29pt@2x.jpg",
19 | "scale" : "2x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "WeScanAppIcon29pt@3x.jpg",
25 | "scale" : "3x"
26 | },
27 | {
28 | "size" : "40x40",
29 | "idiom" : "iphone",
30 | "filename" : "WeScanAppIcon40pt@2x.jpg",
31 | "scale" : "2x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "WeScanAppIcon40pt@3x.jpg",
37 | "scale" : "3x"
38 | },
39 | {
40 | "size" : "60x60",
41 | "idiom" : "iphone",
42 | "filename" : "WeScanAppIcon60pt@2x.jpg",
43 | "scale" : "2x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "WeScanAppIcon60pt@3x.jpg",
49 | "scale" : "3x"
50 | },
51 | {
52 | "idiom" : "ipad",
53 | "size" : "20x20",
54 | "scale" : "1x"
55 | },
56 | {
57 | "idiom" : "ipad",
58 | "size" : "20x20",
59 | "scale" : "2x"
60 | },
61 | {
62 | "idiom" : "ipad",
63 | "size" : "29x29",
64 | "scale" : "1x"
65 | },
66 | {
67 | "idiom" : "ipad",
68 | "size" : "29x29",
69 | "scale" : "2x"
70 | },
71 | {
72 | "idiom" : "ipad",
73 | "size" : "40x40",
74 | "scale" : "1x"
75 | },
76 | {
77 | "idiom" : "ipad",
78 | "size" : "40x40",
79 | "scale" : "2x"
80 | },
81 | {
82 | "idiom" : "ipad",
83 | "size" : "76x76",
84 | "scale" : "1x"
85 | },
86 | {
87 | "idiom" : "ipad",
88 | "size" : "76x76",
89 | "scale" : "2x"
90 | },
91 | {
92 | "idiom" : "ipad",
93 | "size" : "83.5x83.5",
94 | "scale" : "2x"
95 | },
96 | {
97 | "idiom" : "ios-marketing",
98 | "size" : "1024x1024",
99 | "scale" : "1x"
100 | }
101 | ],
102 | "info" : {
103 | "version" : 1,
104 | "author" : "xcode"
105 | }
106 | }
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon20pt@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon20pt@2x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon20pt@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon20pt@3x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon29pt@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon29pt@2x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon29pt@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon29pt@3x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon40pt@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon40pt@2x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon40pt@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon40pt@3x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon60pt@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon60pt@2x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon60pt@3x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/AppIcon.appiconset/WeScanAppIcon60pt@3x.jpg
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "WeScanLogo.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "WeScanLogo@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "WeScanLogo@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo.png
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo@2x.png
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Example/WeScanSampleProject/Assets.xcassets/WeScanLogo.imageset/WeScanLogo@3x.png
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/EditImageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditImageViewController.swift
3 | // WeScanSampleProject
4 | //
5 | // Created by Chawatvish Worrapoj on 8/1/2020
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WeScan
11 |
12 | final class EditImageViewController: UIViewController {
13 |
14 | @IBOutlet private weak var editImageView: UIView!
15 | var captureImage: UIImage!
16 | var quad: Quadrilateral?
17 | var controller: WeScan.EditImageViewController!
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | setupView()
22 | }
23 |
24 | private func setupView() {
25 | controller = WeScan.EditImageViewController(
26 | image: captureImage,
27 | quad: quad,
28 | strokeColor: UIColor(red: (69.0 / 255.0), green: (194.0 / 255.0), blue: (177.0 / 255.0), alpha: 1.0).cgColor
29 | )
30 | controller.view.frame = editImageView.bounds
31 | controller.willMove(toParent: self)
32 | editImageView.addSubview(controller.view)
33 | self.addChild(controller)
34 | controller.didMove(toParent: self)
35 | controller.delegate = self
36 | }
37 |
38 | @IBAction func cropTapped(_ sender: UIButton!) {
39 | controller.cropImage()
40 | }
41 | }
42 |
43 | extension EditImageViewController: EditImageViewDelegate {
44 | func cropped(image: UIImage) {
45 | guard let controller = self.storyboard?
46 | .instantiateViewController(withIdentifier: "ReviewImageView") as? ReviewImageViewController else {
47 | return
48 | }
49 | controller.modalPresentationStyle = .fullScreen
50 | controller.image = image
51 | navigationController?.pushViewController(controller, animated: false)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/HomeViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // WeScanSampleProject
4 | //
5 | // Created by Boris Emorine on 2/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WeScan
11 |
12 | final class HomeViewController: UIViewController {
13 |
14 | private lazy var logoImageView: UIImageView = {
15 | let image = #imageLiteral(resourceName: "WeScanLogo")
16 | let imageView = UIImageView(image: image)
17 | imageView.translatesAutoresizingMaskIntoConstraints = false
18 | return imageView
19 | }()
20 |
21 | private lazy var logoLabel: UILabel = {
22 | let label = UILabel()
23 | label.text = "WeScan"
24 | label.font = UIFont.systemFont(ofSize: 25.0, weight: .bold)
25 | label.textAlignment = .center
26 | label.translatesAutoresizingMaskIntoConstraints = false
27 | return label
28 | }()
29 |
30 | private lazy var scanButton: UIButton = {
31 | let button = UIButton(type: .custom)
32 | button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .headline)
33 | button.setTitle("Scan Item", for: .normal)
34 | button.translatesAutoresizingMaskIntoConstraints = false
35 | button.addTarget(self, action: #selector(scanOrSelectImage(_:)), for: .touchUpInside)
36 | button.backgroundColor = UIColor(red: 64.0 / 255.0, green: 159 / 255.0, blue: 255 / 255.0, alpha: 1.0)
37 | button.layer.cornerRadius = 10.0
38 | return button
39 | }()
40 |
41 | // MARK: - Life Cycle
42 |
43 | override func viewDidLoad() {
44 | super.viewDidLoad()
45 |
46 | setupViews()
47 | setupConstraints()
48 | }
49 |
50 | // MARK: - Setups
51 |
52 | private func setupViews() {
53 | view.addSubview(logoImageView)
54 | view.addSubview(logoLabel)
55 | view.addSubview(scanButton)
56 | }
57 |
58 | private func setupConstraints() {
59 |
60 | let logoImageViewConstraints = [
61 | logoImageView.widthAnchor.constraint(equalToConstant: 150.0),
62 | logoImageView.heightAnchor.constraint(equalToConstant: 150.0),
63 | logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
64 | NSLayoutConstraint(
65 | item: logoImageView,
66 | attribute: .centerY,
67 | relatedBy: .equal,
68 | toItem: view,
69 | attribute: .centerY,
70 | multiplier: 0.75,
71 | constant: 0.0
72 | )
73 | ]
74 |
75 | let logoLabelConstraints = [
76 | logoLabel.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 20.0),
77 | logoLabel.centerXAnchor.constraint(equalTo: logoImageView.centerXAnchor)
78 | ]
79 |
80 | NSLayoutConstraint.activate(logoLabelConstraints + logoImageViewConstraints)
81 |
82 | if #available(iOS 11.0, *) {
83 | let scanButtonConstraints = [
84 | scanButton.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 16),
85 | scanButton.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor, constant: -16),
86 | scanButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
87 | scanButton.heightAnchor.constraint(equalToConstant: 55)
88 | ]
89 |
90 | NSLayoutConstraint.activate(scanButtonConstraints)
91 | } else {
92 | let scanButtonConstraints = [
93 | scanButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16),
94 | scanButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16),
95 | scanButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
96 | scanButton.heightAnchor.constraint(equalToConstant: 55)
97 | ]
98 |
99 | NSLayoutConstraint.activate(scanButtonConstraints)
100 | }
101 | }
102 |
103 | // MARK: - Actions
104 |
105 | @objc func scanOrSelectImage(_ sender: UIButton) {
106 | let actionSheet = UIAlertController(
107 | title: "Would you like to scan an image or select one from your photo library?",
108 | message: nil,
109 | preferredStyle: .actionSheet
110 | )
111 |
112 | let newAction = UIAlertAction(title: "A new scan", style: .default) { _ in
113 | guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "NewCameraViewController") else { return }
114 | controller.modalPresentationStyle = .fullScreen
115 | self.present(controller, animated: true, completion: nil)
116 | }
117 |
118 | let scanAction = UIAlertAction(title: "Scan", style: .default) { _ in
119 | self.scanImage()
120 | }
121 |
122 | let selectAction = UIAlertAction(title: "Select", style: .default) { _ in
123 | self.selectImage()
124 | }
125 |
126 | let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
127 |
128 | actionSheet.addAction(scanAction)
129 | actionSheet.addAction(selectAction)
130 | actionSheet.addAction(cancelAction)
131 | actionSheet.addAction(newAction)
132 |
133 | present(actionSheet, animated: true)
134 | }
135 |
136 | func scanImage() {
137 | let scannerViewController = ImageScannerController(delegate: self)
138 | scannerViewController.modalPresentationStyle = .fullScreen
139 |
140 | if #available(iOS 13.0, *) {
141 | scannerViewController.navigationBar.tintColor = .label
142 | } else {
143 | scannerViewController.navigationBar.tintColor = .black
144 | }
145 |
146 | present(scannerViewController, animated: true)
147 | }
148 |
149 | func selectImage() {
150 | let imagePicker = UIImagePickerController()
151 | imagePicker.delegate = self
152 | imagePicker.sourceType = .photoLibrary
153 | present(imagePicker, animated: true)
154 | }
155 |
156 | }
157 |
158 | extension HomeViewController: ImageScannerControllerDelegate {
159 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) {
160 | assertionFailure("Error occurred: \(error)")
161 | }
162 |
163 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) {
164 | scanner.dismiss(animated: true, completion: nil)
165 | }
166 |
167 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController) {
168 | scanner.dismiss(animated: true, completion: nil)
169 | }
170 |
171 | }
172 |
173 | extension HomeViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
174 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
175 | picker.dismiss(animated: true)
176 | }
177 |
178 | func imagePickerController(_ picker: UIImagePickerController,
179 | didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
180 | picker.dismiss(animated: true)
181 |
182 | guard let image = info[.originalImage] as? UIImage else { return }
183 | let scannerViewController = ImageScannerController(image: image, delegate: self)
184 | present(scannerViewController, animated: true)
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/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 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSCameraUsageDescription
24 | Used to scan images.
25 | NSPhotoLibraryUsageDescription
26 | Used to select images from your Photo Library.
27 | UILaunchStoryboardName
28 | LaunchScreen
29 | UIMainStoryboardFile
30 | Main
31 | UIRequiredDeviceCapabilities
32 |
33 | armv7
34 |
35 | UISupportedInterfaceOrientations
36 |
37 | UIInterfaceOrientationPortrait
38 | UIInterfaceOrientationLandscapeLeft
39 | UIInterfaceOrientationLandscapeRight
40 | UIInterfaceOrientationPortraitUpsideDown
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/NewCameraViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewCameraViewController.swift
3 | // WeScanSampleProject
4 | //
5 | // Created by Chawatvish Worrapoj on 7/1/2020
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import WeScan
11 |
12 | final class NewCameraViewController: UIViewController {
13 |
14 | @IBOutlet private weak var cameraView: UIView!
15 | var controller: CameraScannerViewController!
16 |
17 | override func viewDidLoad() {
18 | super.viewDidLoad()
19 | setupView()
20 | }
21 |
22 | private func setupView() {
23 | controller = CameraScannerViewController()
24 | controller.view.frame = cameraView.bounds
25 | controller.willMove(toParent: self)
26 | cameraView.addSubview(controller.view)
27 | self.addChild(controller)
28 | controller.didMove(toParent: self)
29 | controller.delegate = self
30 | }
31 |
32 | @IBAction func flashTapped(_ sender: UIButton) {
33 | controller.toggleFlash()
34 | }
35 |
36 | @IBAction func captureTapped(_ sender: UIButton) {
37 | controller.capture()
38 | }
39 |
40 | }
41 |
42 | extension NewCameraViewController: CameraScannerViewOutputDelegate {
43 | func captureImageFailWithError(error: Error) {
44 | print(error)
45 | }
46 |
47 | func captureImageSuccess(image: UIImage, withQuad quad: Quadrilateral?) {
48 | guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "NewEditImageView") as? EditImageViewController
49 | else { return }
50 | controller.modalPresentationStyle = .fullScreen
51 | controller.captureImage = image
52 | controller.quad = quad
53 | navigationController?.pushViewController(controller, animated: false)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/ReviewImageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewImageViewController.swift
3 | // WeScanSampleProject
4 | //
5 | // Created by Chawatvish Worrapoj on 8/1/2020
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class ReviewImageViewController: UIViewController {
12 |
13 | @IBOutlet private weak var imageView: UIImageView!
14 | var image: UIImage?
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | guard let image else { return }
19 | imageView.image = image
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/nl.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/Example/WeScanSampleProject/nl.lproj/Main.strings:
--------------------------------------------------------------------------------
1 |
2 | /* Class = "UIButton"; normalTitle = "Crop Image"; ObjectID = "95t-Li-oZf"; */
3 | "95t-Li-oZf.normalTitle" = "Crop Image";
4 |
5 | /* Class = "UIButton"; normalTitle = "Flash"; ObjectID = "YbL-Hk-8Tq"; */
6 | "YbL-Hk-8Tq.normalTitle" = "Flash";
7 |
8 | /* Class = "UIButton"; normalTitle = "Capture"; ObjectID = "aYQ-YN-7Jr"; */
9 | "aYQ-YN-7Jr.normalTitle" = "Capture";
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 WeTransfer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-snapshot-testing",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
7 | "state" : {
8 | "revision" : "f29e2014f6230cf7d5138fc899da51c7f513d467",
9 | "version" : "1.10.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.7
2 | // We're hiding dev, test, and danger dependencies with // dev to make sure they're not fetched by users of this package.
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "WeScan",
7 | defaultLocalization: "en",
8 | platforms: [
9 | .iOS(.v13)
10 | ],
11 | products: [
12 | .library(name: "WeScan", targets: ["WeScan"])
13 | ],
14 | dependencies: [
15 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.10.0")
16 | ],
17 | targets: [
18 | .target(name: "WeScan",
19 | resources: [
20 | .process("Resources")
21 | ]),
22 | .testTarget(
23 | name: "WeScanTests",
24 | dependencies: [
25 | "WeScan",
26 | .product(name: "SnapshotTesting", package: "swift-snapshot-testing")
27 | ],
28 | exclude:["Info.plist"],
29 | resources: [
30 | .process("Resources"),
31 | .copy("__Snapshots__")
32 | ]
33 | )
34 | ]
35 | )
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WeScan
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | **WeScan** makes it easy to add scanning functionalities to your iOS app!
16 | It's modelled after `UIImagePickerController`, which makes it a breeze to use.
17 |
18 | - [Features](#features)
19 | - [Demo](#demo)
20 | - [Requirements](#requirements)
21 | - [Installation](#installation)
22 | - [Usage](#usage)
23 | - [Contributing](#contributing)
24 | - [License](#license)
25 |
26 | ## Features
27 |
28 | - [x] Fast and lightweight
29 | - [x] Live scanning of documents
30 | - [x] Edit detected rectangle
31 | - [x] Auto scan and flash support
32 | - [x] Support for both PDF and UIImage
33 | - [x] Translated to English, Chinese, Italian, Portuguese, and French
34 | - [ ] Batch scanning
35 |
36 | ## Demo
37 |
38 |
39 |
40 |
41 |
42 | ## Requirements
43 |
44 | - Swift 5.0
45 | - iOS 10.0+
46 |
47 |
48 |
49 | ## Installation
50 |
51 | ### Swift Package Manager
52 |
53 | The [Swift Package Manager](https://swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the `swift` compiler. It is in early development, but WeScan does support its use on supported platforms.
54 |
55 | Once you have your Swift package set up, adding WeScan as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`.
56 |
57 | ```swift
58 | dependencies: [
59 | .package(url: "https://github.com/WeTransfer/WeScan.git", .upToNextMajor(from: "2.1.0"))
60 | ]
61 | ```
62 |
63 | ## Usage
64 |
65 | ### Swift
66 |
67 | 1. In order to make the framework available, add `import WeScan` at the top of the Swift source file
68 |
69 | 2. In the Info.plist, add the `NSCameraUsageDescription` key and set the appropriate value in which you have to inform the user of the reason to allow the camera permission
70 |
71 | 3. Make sure that your view controller conforms to the `ImageScannerControllerDelegate` protocol:
72 |
73 | ```swift
74 | class YourViewController: UIViewController, ImageScannerControllerDelegate {
75 | // YourViewController code here
76 | }
77 | ```
78 |
79 | 4. Implement the delegate functions inside your view controller:
80 | ```swift
81 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) {
82 | // You are responsible for carefully handling the error
83 | print(error)
84 | }
85 |
86 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) {
87 | // The user successfully scanned an image, which is available in the ImageScannerResults
88 | // You are responsible for dismissing the ImageScannerController
89 | scanner.dismiss(animated: true)
90 | }
91 |
92 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController) {
93 | // The user tapped 'Cancel' on the scanner
94 | // You are responsible for dismissing the ImageScannerController
95 | scanner.dismiss(animated: true)
96 | }
97 | ```
98 |
99 | 5. Finally, create and present a `ImageScannerController` instance somewhere within your view controller:
100 |
101 | ```swift
102 | let scannerViewController = ImageScannerController()
103 | scannerViewController.imageScannerDelegate = self
104 | present(scannerViewController, animated: true)
105 | ```
106 |
107 | ### Objective-C
108 |
109 | 1. Create a dummy swift class in your project. When Xcode asks if you'd like to create a bridging header, press 'Create Bridging Header'
110 | 2. In the new header, add the Objective-C class (`#import myClass.h`) where you want to use WeScan
111 | 3. In your class, import the header (`import `)
112 | 4. Drag and drop the WeScan folder to add it to your project
113 | 5. In your class, add `@Class ImageScannerController;`
114 |
115 | #### Example Implementation
116 |
117 | ```objc
118 | ImageScannerController *scannerViewController = [[ImageScannerController alloc] init];
119 | [self presentViewController:scannerViewController animated:YES completion:nil];
120 | ```
121 |
122 |
123 |
124 | ## Contributing
125 |
126 | As the creators, and maintainers of this project, we're glad to invite contributors to help us stay up to date. Please take a moment to review [the contributing document](CONTRIBUTING.md) in order to make the contribution process easy and effective for everyone involved.
127 |
128 | - If you **found a bug**, open an [issue](https://github.com/WeTransfer/WeScan/issues).
129 | - If you **have a feature request**, open an [issue](https://github.com/WeTransfer/WeScan/issues).
130 | - If you **want to contribute**, submit a [pull request](https://github.com/WeTransfer/WeScan/pulls).
131 |
132 |
133 |
134 | ## License
135 |
136 | **WeScan** is available under the MIT license. See the [LICENSE](https://github.com/WeTransfer/WeScan/blob/develop/LICENSE) file for more info.
137 |
--------------------------------------------------------------------------------
/Sources/WeScan/Common/CIRectangleDetector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RectangleDetector.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/13/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import CoreImage
11 | import Foundation
12 |
13 | /// Class used to detect rectangles from an image.
14 | enum CIRectangleDetector {
15 |
16 | static let rectangleDetector = CIDetector(ofType: CIDetectorTypeRectangle,
17 | context: CIContext(options: nil),
18 | options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
19 |
20 | /// Detects rectangles from the given image on iOS 10.
21 | ///
22 | /// - Parameters:
23 | /// - image: The image to detect rectangles on.
24 | /// - Returns: The biggest detected rectangle on the image.
25 | static func rectangle(forImage image: CIImage, completion: @escaping ((Quadrilateral?) -> Void)) {
26 | let biggestRectangle = rectangle(forImage: image)
27 | completion(biggestRectangle)
28 | }
29 |
30 | static func rectangle(forImage image: CIImage) -> Quadrilateral? {
31 | guard let rectangleFeatures = rectangleDetector?.features(in: image) as? [CIRectangleFeature] else {
32 | return nil
33 | }
34 |
35 | let quads = rectangleFeatures.map { rectangle in
36 | return Quadrilateral(rectangleFeature: rectangle)
37 | }
38 |
39 | return quads.biggest()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Sources/WeScan/Common/EditScanCornerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditScanCornerView.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 3/5/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreImage
10 | import UIKit
11 |
12 | /// A UIView used by corners of a quadrilateral that is aware of its position.
13 | final class EditScanCornerView: UIView {
14 |
15 | let position: CornerPosition
16 |
17 | /// The image to display when the corner view is highlighted.
18 | private var image: UIImage?
19 | private(set) var isHighlighted = false
20 |
21 | private lazy var circleLayer: CAShapeLayer = {
22 | let layer = CAShapeLayer()
23 | layer.fillColor = UIColor.clear.cgColor
24 | layer.strokeColor = UIColor.white.cgColor
25 | layer.lineWidth = 1.0
26 | return layer
27 | }()
28 |
29 | /// Set stroke color of corner layer
30 | public var strokeColor: CGColor? {
31 | didSet {
32 | circleLayer.strokeColor = strokeColor
33 | }
34 | }
35 |
36 | init(frame: CGRect, position: CornerPosition) {
37 | self.position = position
38 | super.init(frame: frame)
39 | backgroundColor = UIColor.clear
40 | clipsToBounds = true
41 | layer.addSublayer(circleLayer)
42 | }
43 |
44 | required init?(coder aDecoder: NSCoder) {
45 | fatalError("init(coder:) has not been implemented")
46 | }
47 |
48 | override func layoutSubviews() {
49 | super.layoutSubviews()
50 | layer.cornerRadius = bounds.width / 2.0
51 | }
52 |
53 | override func draw(_ rect: CGRect) {
54 | super.draw(rect)
55 |
56 | let bezierPath = UIBezierPath(ovalIn: rect.insetBy(dx: circleLayer.lineWidth, dy: circleLayer.lineWidth))
57 | circleLayer.frame = rect
58 | circleLayer.path = bezierPath.cgPath
59 |
60 | image?.draw(in: rect)
61 | }
62 |
63 | func highlightWithImage(_ image: UIImage) {
64 | isHighlighted = true
65 | self.image = image
66 | self.setNeedsDisplay()
67 | }
68 |
69 | func reset() {
70 | isHighlighted = false
71 | image = nil
72 | setNeedsDisplay()
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/WeScan/Common/Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Error.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/28/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreImage
10 | import Foundation
11 |
12 | /// Errors related to the `ImageScannerController`
13 | public enum ImageScannerControllerError: Error {
14 | /// The user didn't grant permission to use the camera.
15 | case authorization
16 | /// An error occurred when setting up the user's device.
17 | case inputDevice
18 | /// An error occurred when trying to capture a picture.
19 | case capture
20 | /// Error when creating the CIImage.
21 | case ciImageCreation
22 | }
23 |
24 | extension ImageScannerControllerError: LocalizedError {
25 |
26 | public var errorDescription: String? {
27 | switch self {
28 | case .authorization:
29 | return "Failed to get the user's authorization for camera."
30 | case .inputDevice:
31 | return "Could not setup input device."
32 | case .capture:
33 | return "Could not capture picture."
34 | case .ciImageCreation:
35 | return "Internal Error - Could not create CIImage"
36 | }
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/WeScan/Common/Quadrilateral.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Quadrilateral.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import CoreImage
11 | import Foundation
12 | import UIKit
13 | import Vision
14 |
15 | /// A data structure representing a quadrilateral and its position.
16 | /// This class exists to bypass the fact that CIRectangleFeature is read-only.
17 | public struct Quadrilateral: Transformable {
18 |
19 | /// A point that specifies the top left corner of the quadrilateral.
20 | public var topLeft: CGPoint
21 |
22 | /// A point that specifies the top right corner of the quadrilateral.
23 | public var topRight: CGPoint
24 |
25 | /// A point that specifies the bottom right corner of the quadrilateral.
26 | public var bottomRight: CGPoint
27 |
28 | /// A point that specifies the bottom left corner of the quadrilateral.
29 | public var bottomLeft: CGPoint
30 |
31 | public var description: String {
32 | return "topLeft: \(topLeft), topRight: \(topRight), bottomRight: \(bottomRight), bottomLeft: \(bottomLeft)"
33 | }
34 |
35 | /// The path of the Quadrilateral as a `UIBezierPath`
36 | var path: UIBezierPath {
37 | let path = UIBezierPath()
38 | path.move(to: topLeft)
39 | path.addLine(to: topRight)
40 | path.addLine(to: bottomRight)
41 | path.addLine(to: bottomLeft)
42 | path.close()
43 |
44 | return path
45 | }
46 |
47 | /// The perimeter of the Quadrilateral
48 | var perimeter: Double {
49 | let perimeter = topLeft.distanceTo(point: topRight)
50 | + topRight.distanceTo(point: bottomRight)
51 | + bottomRight.distanceTo(point: bottomLeft)
52 | + bottomLeft.distanceTo(point: topLeft)
53 | return Double(perimeter)
54 | }
55 |
56 | init(rectangleFeature: CIRectangleFeature) {
57 | self.topLeft = rectangleFeature.topLeft
58 | self.topRight = rectangleFeature.topRight
59 | self.bottomLeft = rectangleFeature.bottomLeft
60 | self.bottomRight = rectangleFeature.bottomRight
61 | }
62 |
63 | @available(iOS 11.0, *)
64 | init(rectangleObservation: VNRectangleObservation) {
65 | self.topLeft = rectangleObservation.topLeft
66 | self.topRight = rectangleObservation.topRight
67 | self.bottomLeft = rectangleObservation.bottomLeft
68 | self.bottomRight = rectangleObservation.bottomRight
69 | }
70 |
71 | init(topLeft: CGPoint, topRight: CGPoint, bottomRight: CGPoint, bottomLeft: CGPoint) {
72 | self.topLeft = topLeft
73 | self.topRight = topRight
74 | self.bottomRight = bottomRight
75 | self.bottomLeft = bottomLeft
76 | }
77 |
78 | /// Applies a `CGAffineTransform` to the quadrilateral.
79 | ///
80 | /// - Parameters:
81 | /// - t: the transform to apply.
82 | /// - Returns: The transformed quadrilateral.
83 | func applying(_ transform: CGAffineTransform) -> Quadrilateral {
84 | let quadrilateral = Quadrilateral(
85 | topLeft: topLeft.applying(transform),
86 | topRight: topRight.applying(transform),
87 | bottomRight: bottomRight.applying(transform),
88 | bottomLeft: bottomLeft.applying(transform)
89 | )
90 |
91 | return quadrilateral
92 | }
93 |
94 | /// Checks whether the quadrilateral is within a given distance of another quadrilateral.
95 | ///
96 | /// - Parameters:
97 | /// - distance: The distance (threshold) to use for the condition to be met.
98 | /// - rectangleFeature: The other rectangle to compare this instance with.
99 | /// - Returns: True if the given rectangle is within the given distance of this rectangle instance.
100 | func isWithin(_ distance: CGFloat, ofRectangleFeature rectangleFeature: Quadrilateral) -> Bool {
101 |
102 | let topLeftRect = topLeft.surroundingSquare(withSize: distance)
103 | if !topLeftRect.contains(rectangleFeature.topLeft) {
104 | return false
105 | }
106 |
107 | let topRightRect = topRight.surroundingSquare(withSize: distance)
108 | if !topRightRect.contains(rectangleFeature.topRight) {
109 | return false
110 | }
111 |
112 | let bottomRightRect = bottomRight.surroundingSquare(withSize: distance)
113 | if !bottomRightRect.contains(rectangleFeature.bottomRight) {
114 | return false
115 | }
116 |
117 | let bottomLeftRect = bottomLeft.surroundingSquare(withSize: distance)
118 | if !bottomLeftRect.contains(rectangleFeature.bottomLeft) {
119 | return false
120 | }
121 |
122 | return true
123 | }
124 |
125 | /// Reorganizes the current quadrilateral, making sure that the points are at their appropriate positions.
126 | /// For example, it ensures that the top left point is actually the top and left point point of the quadrilateral.
127 | mutating func reorganize() {
128 | let points = [topLeft, topRight, bottomRight, bottomLeft]
129 | let ySortedPoints = sortPointsByYValue(points)
130 |
131 | guard ySortedPoints.count == 4 else {
132 | return
133 | }
134 |
135 | let topMostPoints = Array(ySortedPoints[0..<2])
136 | let bottomMostPoints = Array(ySortedPoints[2..<4])
137 | let xSortedTopMostPoints = sortPointsByXValue(topMostPoints)
138 | let xSortedBottomMostPoints = sortPointsByXValue(bottomMostPoints)
139 |
140 | guard xSortedTopMostPoints.count > 1,
141 | xSortedBottomMostPoints.count > 1 else {
142 | return
143 | }
144 |
145 | topLeft = xSortedTopMostPoints[0]
146 | topRight = xSortedTopMostPoints[1]
147 | bottomRight = xSortedBottomMostPoints[1]
148 | bottomLeft = xSortedBottomMostPoints[0]
149 | }
150 |
151 | /// Scales the quadrilateral based on the ratio of two given sizes, and optionally applies a rotation.
152 | ///
153 | /// - Parameters:
154 | /// - fromSize: The size the quadrilateral is currently related to.
155 | /// - toSize: The size to scale the quadrilateral to.
156 | /// - rotationAngle: The optional rotation to apply.
157 | /// - Returns: The newly scaled and potentially rotated quadrilateral.
158 | func scale(_ fromSize: CGSize, _ toSize: CGSize, withRotationAngle rotationAngle: CGFloat = 0.0) -> Quadrilateral {
159 | var invertedFromSize = fromSize
160 | let rotated = rotationAngle != 0.0
161 |
162 | if rotated && rotationAngle != CGFloat.pi {
163 | invertedFromSize = CGSize(width: fromSize.height, height: fromSize.width)
164 | }
165 |
166 | var transformedQuad = self
167 | let invertedFromSizeWidth = invertedFromSize.width == 0 ? .leastNormalMagnitude : invertedFromSize.width
168 | let invertedFromSizeHeight = invertedFromSize.height == 0 ? .leastNormalMagnitude : invertedFromSize.height
169 |
170 | let scaleWidth = toSize.width / invertedFromSizeWidth
171 | let scaleHeight = toSize.height / invertedFromSizeHeight
172 | let scaledTransform = CGAffineTransform(scaleX: scaleWidth, y: scaleHeight)
173 | transformedQuad = transformedQuad.applying(scaledTransform)
174 |
175 | if rotated {
176 | let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle)
177 |
178 | let fromImageBounds = CGRect(origin: .zero, size: fromSize).applying(scaledTransform).applying(rotationTransform)
179 |
180 | let toImageBounds = CGRect(origin: .zero, size: toSize)
181 | let translationTransform = CGAffineTransform.translateTransform(
182 | fromCenterOfRect: fromImageBounds,
183 | toCenterOfRect: toImageBounds
184 | )
185 |
186 | transformedQuad = transformedQuad.applyTransforms([rotationTransform, translationTransform])
187 | }
188 |
189 | return transformedQuad
190 | }
191 |
192 | // Convenience functions
193 |
194 | /// Sorts the given `CGPoints` based on their y value.
195 | /// - Parameters:
196 | /// - points: The points to sort.
197 | /// - Returns: The points sorted based on their y value.
198 | private func sortPointsByYValue(_ points: [CGPoint]) -> [CGPoint] {
199 | return points.sorted { point1, point2 -> Bool in
200 | point1.y < point2.y
201 | }
202 | }
203 |
204 | /// Sorts the given `CGPoints` based on their x value.
205 | /// - Parameters:
206 | /// - points: The points to sort.
207 | /// - Returns: The points sorted based on their x value.
208 | private func sortPointsByXValue(_ points: [CGPoint]) -> [CGPoint] {
209 | return points.sorted { point1, point2 -> Bool in
210 | point1.x < point2.x
211 | }
212 | }
213 | }
214 |
215 | extension Quadrilateral {
216 |
217 | /// Converts the current to the cartesian coordinate system (where 0 on the y axis is at the bottom).
218 | ///
219 | /// - Parameters:
220 | /// - height: The height of the rect containing the quadrilateral.
221 | /// - Returns: The same quadrilateral in the cartesian coordinate system.
222 | func toCartesian(withHeight height: CGFloat) -> Quadrilateral {
223 | let topLeft = self.topLeft.cartesian(withHeight: height)
224 | let topRight = self.topRight.cartesian(withHeight: height)
225 | let bottomRight = self.bottomRight.cartesian(withHeight: height)
226 | let bottomLeft = self.bottomLeft.cartesian(withHeight: height)
227 |
228 | return Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
229 | }
230 | }
231 |
232 | extension Quadrilateral: Equatable {
233 | public static func == (lhs: Quadrilateral, rhs: Quadrilateral) -> Bool {
234 | return lhs.topLeft == rhs.topLeft
235 | && lhs.topRight == rhs.topRight
236 | && lhs.bottomRight == rhs.bottomRight
237 | && lhs.bottomLeft == rhs.bottomLeft
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/Sources/WeScan/Common/VisionRectangleDetector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisionRectangleDetector.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 28/7/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreImage
10 | import Foundation
11 | import Vision
12 |
13 | /// Enum encapsulating static functions to detect rectangles from an image.
14 | @available(iOS 11.0, *)
15 | enum VisionRectangleDetector {
16 |
17 | private static func completeImageRequest(
18 | for request: VNImageRequestHandler,
19 | width: CGFloat,
20 | height: CGFloat,
21 | completion: @escaping ((Quadrilateral?) -> Void)
22 | ) {
23 | // Create the rectangle request, and, if found, return the biggest rectangle (else return nothing).
24 | let rectangleDetectionRequest: VNDetectRectanglesRequest = {
25 | let rectDetectRequest = VNDetectRectanglesRequest(completionHandler: { request, error in
26 | guard error == nil, let results = request.results as? [VNRectangleObservation], !results.isEmpty else {
27 | completion(nil)
28 | return
29 | }
30 |
31 | let quads: [Quadrilateral] = results.map(Quadrilateral.init)
32 |
33 | // This can't fail because the earlier guard protected against an empty array, but we use guard because of SwiftLint
34 | guard let biggest = quads.biggest() else {
35 | completion(nil)
36 | return
37 | }
38 |
39 | let transform = CGAffineTransform.identity
40 | .scaledBy(x: width, y: height)
41 |
42 | completion(biggest.applying(transform))
43 | })
44 |
45 | rectDetectRequest.minimumConfidence = 0.8
46 | rectDetectRequest.maximumObservations = 15
47 | rectDetectRequest.minimumAspectRatio = 0.3
48 |
49 | return rectDetectRequest
50 | }()
51 |
52 | // Send the requests to the request handler.
53 | do {
54 | try request.perform([rectangleDetectionRequest])
55 | } catch {
56 | completion(nil)
57 | return
58 | }
59 |
60 | }
61 |
62 | /// Detects rectangles from the given CVPixelBuffer/CVImageBuffer on iOS 11 and above.
63 | ///
64 | /// - Parameters:
65 | /// - pixelBuffer: The pixelBuffer to detect rectangles on.
66 | /// - completion: The biggest rectangle on the CVPixelBuffer
67 | static func rectangle(forPixelBuffer pixelBuffer: CVPixelBuffer, completion: @escaping ((Quadrilateral?) -> Void)) {
68 | let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
69 | VisionRectangleDetector.completeImageRequest(
70 | for: imageRequestHandler,
71 | width: CGFloat(CVPixelBufferGetWidth(pixelBuffer)),
72 | height: CGFloat(CVPixelBufferGetHeight(pixelBuffer)),
73 | completion: completion)
74 | }
75 |
76 | /// Detects rectangles from the given image on iOS 11 and above.
77 | ///
78 | /// - Parameters:
79 | /// - image: The image to detect rectangles on.
80 | /// - Returns: The biggest rectangle detected on the image.
81 | static func rectangle(forImage image: CIImage, completion: @escaping ((Quadrilateral?) -> Void)) {
82 | let imageRequestHandler = VNImageRequestHandler(ciImage: image, options: [:])
83 | VisionRectangleDetector.completeImageRequest(
84 | for: imageRequestHandler, width: image.extent.width,
85 | height: image.extent.height, completion: completion)
86 | }
87 |
88 | static func rectangle(
89 | forImage image: CIImage,
90 | orientation: CGImagePropertyOrientation,
91 | completion: @escaping ((Quadrilateral?) -> Void)
92 | ) {
93 | let imageRequestHandler = VNImageRequestHandler(ciImage: image, orientation: orientation, options: [:])
94 | let orientedImage = image.oriented(orientation)
95 | VisionRectangleDetector.completeImageRequest(
96 | for: imageRequestHandler, width: orientedImage.extent.width,
97 | height: orientedImage.extent.height, completion: completion)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/WeScan/Edit/EditImageViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EditImageViewController.swift
3 | // WeScan
4 | //
5 | // Created by Chawatvish Worrapoj on 7/1/2020
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import UIKit
11 |
12 | /// A protocol that your delegate object will get results of EditImageViewController.
13 | public protocol EditImageViewDelegate: AnyObject {
14 | /// A method that your delegate object must implement to get cropped image.
15 | func cropped(image: UIImage)
16 | }
17 |
18 | /// A view controller that manages edit image for scanning documents or pick image from photo library
19 | /// The `EditImageViewController` class is individual for rotate, crop image
20 | public final class EditImageViewController: UIViewController {
21 |
22 | /// The image the quadrilateral was detected on.
23 | private var image: UIImage
24 |
25 | /// The detected quadrilateral that can be edited by the user. Uses the image's coordinates.
26 | private var quad: Quadrilateral
27 | private var zoomGestureController: ZoomGestureController!
28 | private var quadViewWidthConstraint = NSLayoutConstraint()
29 | private var quadViewHeightConstraint = NSLayoutConstraint()
30 | public weak var delegate: EditImageViewDelegate?
31 |
32 | private lazy var imageView: UIImageView = {
33 | let imageView = UIImageView()
34 | imageView.clipsToBounds = true
35 | imageView.isOpaque = true
36 | imageView.image = image
37 | imageView.backgroundColor = .black
38 | imageView.contentMode = .scaleAspectFit
39 | imageView.translatesAutoresizingMaskIntoConstraints = false
40 | return imageView
41 | }()
42 |
43 | private lazy var quadView: QuadrilateralView = {
44 | let quadView = QuadrilateralView()
45 | quadView.editable = true
46 | quadView.strokeColor = strokeColor
47 | quadView.translatesAutoresizingMaskIntoConstraints = false
48 | return quadView
49 | }()
50 |
51 | private var strokeColor: CGColor?
52 |
53 | // MARK: - Life Cycle
54 |
55 | public init(image: UIImage, quad: Quadrilateral?, rotateImage: Bool = true, strokeColor: CGColor? = nil) {
56 | self.image = rotateImage ? image.applyingPortraitOrientation() : image
57 | self.quad = quad ?? EditImageViewController.defaultQuad(allOfImage: image)
58 | self.strokeColor = strokeColor
59 | super.init(nibName: nil, bundle: nil)
60 | }
61 |
62 | public required init?(coder aDecoder: NSCoder) {
63 | fatalError("init(coder:) has not been implemented")
64 | }
65 |
66 | override public func viewDidLoad() {
67 | super.viewDidLoad()
68 |
69 | setupViews()
70 | setupConstraints()
71 | zoomGestureController = ZoomGestureController(image: image, quadView: quadView)
72 | addLongGesture(of: zoomGestureController)
73 | }
74 |
75 | override public func viewDidLayoutSubviews() {
76 | super.viewDidLayoutSubviews()
77 | adjustQuadViewConstraints()
78 | displayQuad()
79 | }
80 |
81 | // MARK: - Setups
82 |
83 | private func setupViews() {
84 | view.addSubview(imageView)
85 | view.addSubview(quadView)
86 | }
87 |
88 | private func setupConstraints() {
89 | let imageViewConstraints = [
90 | imageView.topAnchor.constraint(equalTo: view.topAnchor),
91 | imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
92 | view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
93 | view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor)
94 | ]
95 |
96 | quadViewWidthConstraint = quadView.widthAnchor.constraint(equalToConstant: 0.0)
97 | quadViewHeightConstraint = quadView.heightAnchor.constraint(equalToConstant: 0.0)
98 |
99 | let quadViewConstraints = [
100 | quadView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
101 | quadView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
102 | quadViewWidthConstraint,
103 | quadViewHeightConstraint
104 | ]
105 |
106 | NSLayoutConstraint.activate(quadViewConstraints + imageViewConstraints)
107 | }
108 |
109 | private func addLongGesture(of controller: ZoomGestureController) {
110 | let touchDown = UILongPressGestureRecognizer(target: controller,
111 | action: #selector(controller.handle(pan:)))
112 | touchDown.minimumPressDuration = 0
113 | view.addGestureRecognizer(touchDown)
114 | }
115 |
116 | // MARK: - Actions
117 | /// This function allow user can crop image follow quad. the image will send back by delegate function
118 | public func cropImage() {
119 | guard let quad = quadView.quad, let ciImage = CIImage(image: image) else {
120 | return
121 | }
122 |
123 | let cgOrientation = CGImagePropertyOrientation(image.imageOrientation)
124 | let orientedImage = ciImage.oriented(forExifOrientation: Int32(cgOrientation.rawValue))
125 | let scaledQuad = quad.scale(quadView.bounds.size, image.size)
126 | self.quad = scaledQuad
127 |
128 | // Cropped Image
129 | var cartesianScaledQuad = scaledQuad.toCartesian(withHeight: image.size.height)
130 | cartesianScaledQuad.reorganize()
131 |
132 | let filteredImage = orientedImage.applyingFilter("CIPerspectiveCorrection", parameters: [
133 | "inputTopLeft": CIVector(cgPoint: cartesianScaledQuad.bottomLeft),
134 | "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight),
135 | "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft),
136 | "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight)
137 | ])
138 |
139 | let croppedImage = UIImage.from(ciImage: filteredImage)
140 | delegate?.cropped(image: croppedImage)
141 | }
142 |
143 | /// This function allow user to rotate image by 90 degree each and will reload image on image view.
144 | public func rotateImage() {
145 | let rotationAngle = Measurement(value: 90, unit: .degrees)
146 | reloadImage(withAngle: rotationAngle)
147 | }
148 |
149 | private func reloadImage(withAngle angle: Measurement) {
150 | guard let newImage = image.rotated(by: angle) else { return }
151 | let newQuad = EditImageViewController.defaultQuad(allOfImage: newImage)
152 |
153 | image = newImage
154 | imageView.image = image
155 | quad = newQuad
156 | adjustQuadViewConstraints()
157 | displayQuad()
158 |
159 | zoomGestureController = ZoomGestureController(image: image, quadView: quadView)
160 | addLongGesture(of: zoomGestureController)
161 | }
162 |
163 | private func displayQuad() {
164 | let imageSize = image.size
165 | let size = CGSize(width: quadViewWidthConstraint.constant, height: quadViewHeightConstraint.constant)
166 | let imageFrame = CGRect(origin: quadView.frame.origin, size: size)
167 |
168 | let scaleTransform = CGAffineTransform.scaleTransform(forSize: imageSize, aspectFillInSize: imageFrame.size)
169 | let transforms = [scaleTransform]
170 | let transformedQuad = quad.applyTransforms(transforms)
171 |
172 | quadView.drawQuadrilateral(quad: transformedQuad, animated: false)
173 | }
174 |
175 | /// The quadView should be lined up on top of the actual image displayed by the imageView.
176 | /// Since there is no way to know the size of that image before run time, we adjust the constraints
177 | /// to make sure that the quadView is on top of the displayed image.
178 | private func adjustQuadViewConstraints() {
179 | let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
180 | quadViewWidthConstraint.constant = frame.size.width
181 | quadViewHeightConstraint.constant = frame.size.height
182 | }
183 |
184 | /// Generates a `Quadrilateral` object that's centered and one third of the size of the passed in image.
185 | private static func defaultQuad(forImage image: UIImage) -> Quadrilateral {
186 | let topLeft = CGPoint(x: image.size.width / 3.0, y: image.size.height / 3.0)
187 | let topRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: image.size.height / 3.0)
188 | let bottomRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: 2.0 * image.size.height / 3.0)
189 | let bottomLeft = CGPoint(x: image.size.width / 3.0, y: 2.0 * image.size.height / 3.0)
190 |
191 | let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
192 |
193 | return quad
194 | }
195 |
196 | /// Generates a `Quadrilateral` object that's cover all of image.
197 | private static func defaultQuad(allOfImage image: UIImage, withOffset offset: CGFloat = 75) -> Quadrilateral {
198 | let topLeft = CGPoint(x: offset, y: offset)
199 | let topRight = CGPoint(x: image.size.width - offset, y: offset)
200 | let bottomRight = CGPoint(x: image.size.width - offset, y: image.size.height - offset)
201 | let bottomLeft = CGPoint(x: offset, y: image.size.height - offset)
202 | let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
203 | return quad
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Sources/WeScan/Edit/ZoomGestureController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ZoomGestureController.swift
3 | // WeScan
4 | //
5 | // Created by Bobo on 5/31/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import Foundation
11 | import UIKit
12 |
13 | final class ZoomGestureController {
14 |
15 | private let image: UIImage
16 | private let quadView: QuadrilateralView
17 | private var previousPanPosition: CGPoint?
18 | private var closestCorner: CornerPosition?
19 |
20 | init(image: UIImage, quadView: QuadrilateralView) {
21 | self.image = image
22 | self.quadView = quadView
23 | }
24 |
25 | @objc func handle(pan: UIGestureRecognizer) {
26 | guard let drawnQuad = quadView.quad else {
27 | return
28 | }
29 |
30 | guard pan.state != .ended else {
31 | self.previousPanPosition = nil
32 | self.closestCorner = nil
33 | quadView.resetHighlightedCornerViews()
34 | return
35 | }
36 |
37 | let position = pan.location(in: quadView)
38 |
39 | let previousPanPosition = self.previousPanPosition ?? position
40 | let closestCorner = self.closestCorner ?? position.closestCornerFrom(quad: drawnQuad)
41 |
42 | let offset = CGAffineTransform(translationX: position.x - previousPanPosition.x, y: position.y - previousPanPosition.y)
43 | let cornerView = quadView.cornerViewForCornerPosition(position: closestCorner)
44 | let draggedCornerViewCenter = cornerView.center.applying(offset)
45 |
46 | quadView.moveCorner(cornerView: cornerView, atPoint: draggedCornerViewCenter)
47 |
48 | self.previousPanPosition = position
49 | self.closestCorner = closestCorner
50 |
51 | let scale = image.size.width / quadView.bounds.size.width
52 | let scaledDraggedCornerViewCenter = CGPoint(x: draggedCornerViewCenter.x * scale, y: draggedCornerViewCenter.y * scale)
53 | guard let zoomedImage = image.scaledImage(
54 | atPoint: scaledDraggedCornerViewCenter,
55 | scaleFactor: 2.5,
56 | targetSize: quadView.bounds.size
57 | ) else {
58 | return
59 | }
60 |
61 | quadView.highlightCornerAtPosition(position: closestCorner, with: zoomedImage)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIDeviceOrientation+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/13/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import CoreImage
11 | import Foundation
12 | import UIKit
13 |
14 | extension AVCaptureVideoOrientation {
15 |
16 | /// Maps UIDeviceOrientation to AVCaptureVideoOrientation
17 | init?(deviceOrientation: UIDeviceOrientation) {
18 | switch deviceOrientation {
19 | case .portrait:
20 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
21 | case .portraitUpsideDown:
22 | self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue)
23 | case .landscapeLeft:
24 | self.init(rawValue: AVCaptureVideoOrientation.landscapeLeft.rawValue)
25 | case .landscapeRight:
26 | self.init(rawValue: AVCaptureVideoOrientation.landscapeRight.rawValue)
27 | case .faceUp:
28 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
29 | case .faceDown:
30 | self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue)
31 | default:
32 | self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue)
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/Array+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import Vision
11 |
12 | extension Array where Element == Quadrilateral {
13 |
14 | /// Finds the biggest rectangle within an array of `Quadrilateral` objects.
15 | func biggest() -> Quadrilateral? {
16 | let biggestRectangle = self.max(by: { rect1, rect2 -> Bool in
17 | return rect1.perimeter < rect2.perimeter
18 | })
19 |
20 | return biggestRectangle
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CGAffineTransform+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransform+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/15/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreImage
10 | import Foundation
11 | import UIKit
12 |
13 | extension CGAffineTransform {
14 |
15 | /// Convenience function to easily get a scale `CGAffineTransform` instance.
16 | ///
17 | /// - Parameters:
18 | /// - fromSize: The size that needs to be transformed to fit (aspect fill) in the other given size.
19 | /// - toSize: The size that should be matched by the `fromSize` parameter.
20 | /// - Returns: The transform that will make the `fromSize` parameter fir (aspect fill) inside the `toSize` parameter.
21 | static func scaleTransform(forSize fromSize: CGSize, aspectFillInSize toSize: CGSize) -> CGAffineTransform {
22 | let scale = max(toSize.width / fromSize.width, toSize.height / fromSize.height)
23 | return CGAffineTransform(scaleX: scale, y: scale)
24 | }
25 |
26 | /// Convenience function to easily get a translate `CGAffineTransform` instance.
27 | ///
28 | /// - Parameters:
29 | /// - fromRect: The rect which center needs to be translated to the center of the other passed in rect.
30 | /// - toRect: The rect that should be matched.
31 | /// - Returns: The transform that will translate the center of the `fromRect` parameter to the center of the `toRect` parameter.
32 | static func translateTransform(fromCenterOfRect fromRect: CGRect, toCenterOfRect toRect: CGRect) -> CGAffineTransform {
33 | let translate = CGPoint(x: toRect.midX - fromRect.midX, y: toRect.midY - fromRect.midY)
34 | return CGAffineTransform(translationX: translate.x, y: translate.y)
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CGImagePropertyOrientation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGImagePropertyOrientation.swift
3 | // WeScan
4 | //
5 | // Created by Yang Chen on 5/21/19.
6 | // Copyright © 2019 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension CGImagePropertyOrientation {
13 | init(_ uiOrientation: UIImage.Orientation) {
14 | switch uiOrientation {
15 | case .up:
16 | self = .up
17 | case .upMirrored:
18 | self = .upMirrored
19 | case .down:
20 | self = .down
21 | case .downMirrored:
22 | self = .downMirrored
23 | case .left:
24 | self = .left
25 | case .leftMirrored:
26 | self = .leftMirrored
27 | case .right:
28 | self = .right
29 | case .rightMirrored:
30 | self = .rightMirrored
31 | @unknown default:
32 | assertionFailure("Unknown orientation, falling to default")
33 | self = .right
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CGPoint+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGPoint+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/9/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension CGPoint {
13 |
14 | /// Returns a rectangle of a given size surrounding the point.
15 | ///
16 | /// - Parameters:
17 | /// - size: The size of the rectangle that should surround the points.
18 | /// - Returns: A `CGRect` instance that surrounds this instance of `CGPoint`.
19 | func surroundingSquare(withSize size: CGFloat) -> CGRect {
20 | return CGRect(x: x - size / 2.0, y: y - size / 2.0, width: size, height: size)
21 | }
22 |
23 | /// Checks wether this point is within a given distance of another point.
24 | ///
25 | /// - Parameters:
26 | /// - delta: The minimum distance to meet for this distance to return true.
27 | /// - point: The second point to compare this instance with.
28 | /// - Returns: True if the given `CGPoint` is within the given distance of this instance of `CGPoint`.
29 | func isWithin(delta: CGFloat, ofPoint point: CGPoint) -> Bool {
30 | return (abs(x - point.x) <= delta) && (abs(y - point.y) <= delta)
31 | }
32 |
33 | /// Returns the same `CGPoint` in the cartesian coordinate system.
34 | ///
35 | /// - Parameters:
36 | /// - height: The height of the bounds this points belong to, in the current coordinate system.
37 | /// - Returns: The same point in the cartesian coordinate system.
38 | func cartesian(withHeight height: CGFloat) -> CGPoint {
39 | return CGPoint(x: x, y: height - y)
40 | }
41 |
42 | /// Returns the distance between two points
43 | func distanceTo(point: CGPoint) -> CGFloat {
44 | return hypot((self.x - point.x), (self.y - point.y))
45 | }
46 |
47 | /// Returns the closest corner from the point
48 | func closestCornerFrom(quad: Quadrilateral) -> CornerPosition {
49 | var smallestDistance = distanceTo(point: quad.topLeft)
50 | var closestCorner = CornerPosition.topLeft
51 |
52 | if distanceTo(point: quad.topRight) < smallestDistance {
53 | smallestDistance = distanceTo(point: quad.topRight)
54 | closestCorner = .topRight
55 | }
56 |
57 | if distanceTo(point: quad.bottomRight) < smallestDistance {
58 | smallestDistance = distanceTo(point: quad.bottomRight)
59 | closestCorner = .bottomRight
60 | }
61 |
62 | if distanceTo(point: quad.bottomLeft) < smallestDistance {
63 | smallestDistance = distanceTo(point: quad.bottomLeft)
64 | closestCorner = .bottomLeft
65 | }
66 |
67 | return closestCorner
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CGRect+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRect+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/26/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension CGRect {
13 |
14 | /// Returns a new `CGRect` instance scaled up or down, with the same center as the original `CGRect` instance.
15 | /// - Parameters:
16 | /// - ratio: The ratio to scale the `CGRect` instance by.
17 | /// - Returns: A new instance of `CGRect` scaled by the given ratio and centered with the original rect.
18 | func scaleAndCenter(withRatio ratio: CGFloat) -> CGRect {
19 | let scaleTransform = CGAffineTransform(scaleX: ratio, y: ratio)
20 | let scaledRect = applying(scaleTransform)
21 |
22 | let translateTransform = CGAffineTransform(
23 | translationX: origin.x * (1 - ratio) + (width - scaledRect.width) / 2.0,
24 | y: origin.y * (1 - ratio) + (height - scaledRect.height) / 2.0
25 | )
26 | let translatedRect = scaledRect.applying(translateTransform)
27 |
28 | return translatedRect
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CGSize+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGSize+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 17/2/2019.
6 | // Copyright © 2019 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension CGSize {
13 | /// Calculates an appropriate scale factor which makes the size fit inside both the `maxWidth` and `maxHeight`.
14 | /// - Parameters:
15 | /// - maxWidth: The maximum width that the size should have after applying the scale factor.
16 | /// - maxHeight: The maximum height that the size should have after applying the scale factor.
17 | /// - Returns: A scale factor that makes the size fit within the `maxWidth` and `maxHeight`.
18 | func scaleFactor(forMaxWidth maxWidth: CGFloat, maxHeight: CGFloat) -> CGFloat {
19 | if width < maxWidth && height < maxHeight { return 1 }
20 |
21 | let widthScaleFactor = 1 / (width / maxWidth)
22 | let heightScaleFactor = 1 / (height / maxHeight)
23 |
24 | // Use the smaller scale factor to ensure both the width and height are below the max
25 | return min(widthScaleFactor, heightScaleFactor)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/CIImage+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CIImage+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 14/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreImage
10 | import Foundation
11 | import UIKit
12 |
13 | extension CIImage {
14 | /// Applies an AdaptiveThresholding filter to the image, which enhances the image and makes it completely gray scale
15 | func applyingAdaptiveThreshold() -> UIImage? {
16 | guard let colorKernel = CIColorKernel(source:
17 | """
18 | kernel vec4 color(__sample pixel, float inputEdgeO, float inputEdge1)
19 | {
20 | float luma = dot(pixel.rgb, vec3(0.2126, 0.7152, 0.0722));
21 | float threshold = smoothstep(inputEdgeO, inputEdge1, luma);
22 | return vec4(threshold, threshold, threshold, 1.0);
23 | }
24 | """
25 | ) else { return nil }
26 |
27 | let firstInputEdge = 0.25
28 | let secondInputEdge = 0.75
29 |
30 | let arguments: [Any] = [self, firstInputEdge, secondInputEdge]
31 |
32 | guard let enhancedCIImage = colorKernel.apply(extent: self.extent, arguments: arguments) else { return nil }
33 |
34 | if let cgImage = CIContext(options: nil).createCGImage(enhancedCIImage, from: enhancedCIImage.extent) {
35 | return UIImage(cgImage: cgImage)
36 | } else {
37 | return UIImage(ciImage: enhancedCIImage, scale: 1.0, orientation: .up)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/UIImage+Orientation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Orientation.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/16/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIImage {
13 |
14 | /// Data structure to easily express rotation options.
15 | struct RotationOptions: OptionSet {
16 | let rawValue: Int
17 |
18 | static let flipOnVerticalAxis = RotationOptions(rawValue: 1)
19 | static let flipOnHorizontalAxis = RotationOptions(rawValue: 2)
20 | }
21 |
22 | /// Returns the same image with a portrait orientation.
23 | func applyingPortraitOrientation() -> UIImage {
24 | switch imageOrientation {
25 | case .up:
26 | return rotated(by: Measurement(value: Double.pi, unit: .radians), options: []) ?? self
27 | case .down:
28 | return rotated(by: Measurement(value: Double.pi, unit: .radians), options: [.flipOnVerticalAxis, .flipOnHorizontalAxis]) ?? self
29 | case .left:
30 | return self
31 | case .right:
32 | return rotated(by: Measurement(value: Double.pi / 2.0, unit: .radians), options: []) ?? self
33 | default:
34 | return self
35 | }
36 | }
37 |
38 | /// Rotate the image by the given angle, and perform other transformations based on the passed in options.
39 | ///
40 | /// - Parameters:
41 | /// - rotationAngle: The angle to rotate the image by.
42 | /// - options: Options to apply to the image.
43 | /// - Returns: The new image rotated and optionally flipped (@see options).
44 | func rotated(by rotationAngle: Measurement, options: RotationOptions = []) -> UIImage? {
45 | guard let cgImage = self.cgImage else { return nil }
46 |
47 | let rotationInRadians = CGFloat(rotationAngle.converted(to: .radians).value)
48 | let transform = CGAffineTransform(rotationAngle: rotationInRadians)
49 | let cgImageSize = CGSize(width: cgImage.width, height: cgImage.height)
50 | var rect = CGRect(origin: .zero, size: cgImageSize).applying(transform)
51 | rect.origin = .zero
52 |
53 | let format = UIGraphicsImageRendererFormat()
54 | format.scale = 1
55 |
56 | let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
57 |
58 | let image = renderer.image { renderContext in
59 | renderContext.cgContext.translateBy(x: rect.midX, y: rect.midY)
60 | renderContext.cgContext.rotate(by: rotationInRadians)
61 |
62 | let x = options.contains(.flipOnVerticalAxis) ? -1.0 : 1.0
63 | let y = options.contains(.flipOnHorizontalAxis) ? 1.0 : -1.0
64 | renderContext.cgContext.scaleBy(x: CGFloat(x), y: CGFloat(y))
65 |
66 | let drawRect = CGRect(origin: CGPoint(x: -cgImageSize.width / 2.0, y: -cgImageSize.height / 2.0), size: cgImageSize)
67 | renderContext.cgContext.draw(cgImage, in: drawRect)
68 | }
69 |
70 | return image
71 | }
72 |
73 | /// Rotates the image based on the information collected by the accelerometer
74 | func withFixedOrientation() -> UIImage {
75 | var imageAngle: Double = 0.0
76 |
77 | var shouldRotate = true
78 | switch CaptureSession.current.editImageOrientation {
79 | case .up:
80 | shouldRotate = false
81 | case .left:
82 | imageAngle = Double.pi / 2
83 | case .right:
84 | imageAngle = -(Double.pi / 2)
85 | case .down:
86 | imageAngle = Double.pi
87 | default:
88 | shouldRotate = false
89 | }
90 |
91 | if shouldRotate,
92 | let finalImage = rotated(by: Measurement(value: imageAngle, unit: .radians)) {
93 | return finalImage
94 | } else {
95 | return self
96 | }
97 | }
98 |
99 | }
100 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/UIImage+SFSymbol.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+SFSymbol.swift
3 | // WeScan
4 | //
5 | // Created by André Schmidt on 19/06/2020.
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension UIImage {
12 |
13 | /// Creates an image object containing a system symbol image appropriate for the specified traits if supported (iOS13 and above).
14 | /// Otherwise an image object using the named image asset that is compatible with the specified trait collection will be created.
15 | convenience init?(
16 | systemName: String,
17 | named: String,
18 | in bundle: Bundle? = nil,
19 | compatibleWith traitCollection: UITraitCollection? = nil
20 | ) {
21 | if #available(iOS 13.0, *) {
22 | self.init(systemName: systemName, compatibleWith: traitCollection)
23 | } else {
24 | self.init(named: named, in: bundle, compatibleWith: traitCollection)
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/WeScan/Extensions/UIImage+Utils.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImage+Utils.swift
3 | // WeScan
4 | //
5 | // Created by Bobo on 5/25/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | extension UIImage {
13 | /// Draws a new cropped and scaled (zoomed in) image.
14 | ///
15 | /// - Parameters:
16 | /// - point: The center of the new image.
17 | /// - scaleFactor: Factor by which the image should be zoomed in.
18 | /// - size: The size of the rect the image will be displayed in.
19 | /// - Returns: The scaled and cropped image.
20 | func scaledImage(atPoint point: CGPoint, scaleFactor: CGFloat, targetSize size: CGSize) -> UIImage? {
21 | guard let cgImage = self.cgImage else { return nil }
22 |
23 | let scaledSize = CGSize(width: size.width / scaleFactor, height: size.height / scaleFactor)
24 | let midX = point.x - scaledSize.width / 2.0
25 | let midY = point.y - scaledSize.height / 2.0
26 | let newRect = CGRect(x: midX, y: midY, width: scaledSize.width, height: scaledSize.height)
27 |
28 | guard let croppedImage = cgImage.cropping(to: newRect) else {
29 | return nil
30 | }
31 |
32 | return UIImage(cgImage: croppedImage)
33 | }
34 |
35 | /// Scales the image to the specified size in the RGB color space.
36 | ///
37 | /// - Parameters:
38 | /// - scaleFactor: Factor by which the image should be scaled.
39 | /// - Returns: The scaled image.
40 | func scaledImage(scaleFactor: CGFloat) -> UIImage? {
41 | guard let cgImage = self.cgImage else { return nil }
42 |
43 | let customColorSpace = CGColorSpaceCreateDeviceRGB()
44 |
45 | let width = CGFloat(cgImage.width) * scaleFactor
46 | let height = CGFloat(cgImage.height) * scaleFactor
47 | let bitsPerComponent = cgImage.bitsPerComponent
48 | let bytesPerRow = cgImage.bytesPerRow
49 | let bitmapInfo = cgImage.bitmapInfo.rawValue
50 |
51 | guard let context = CGContext(
52 | data: nil,
53 | width: Int(width),
54 | height: Int(height),
55 | bitsPerComponent: bitsPerComponent,
56 | bytesPerRow: bytesPerRow,
57 | space: customColorSpace,
58 | bitmapInfo: bitmapInfo
59 | ) else { return nil }
60 |
61 | context.interpolationQuality = .high
62 | context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: width, height: height)))
63 |
64 | return context.makeImage().flatMap { UIImage(cgImage: $0) }
65 | }
66 |
67 | /// Returns the data for the image in the PDF format
68 | func pdfData() -> Data? {
69 | // Typical Letter PDF page size and margins
70 | let pageBounds = CGRect(x: 0, y: 0, width: 595, height: 842)
71 | let margin: CGFloat = 40
72 |
73 | let imageMaxWidth = pageBounds.width - (margin * 2)
74 | let imageMaxHeight = pageBounds.height - (margin * 2)
75 |
76 | let image = scaledImage(scaleFactor: size.scaleFactor(forMaxWidth: imageMaxWidth, maxHeight: imageMaxHeight)) ?? self
77 | let renderer = UIGraphicsPDFRenderer(bounds: pageBounds)
78 |
79 | let data = renderer.pdfData { ctx in
80 | ctx.beginPage()
81 |
82 | ctx.cgContext.interpolationQuality = .high
83 |
84 | image.draw(at: CGPoint(x: margin, y: margin))
85 | }
86 |
87 | return data
88 | }
89 |
90 | /// Function gathered from [here](https://stackoverflow.com/questions/44462087/how-to-convert-a-uiimage-to-a-cvpixelbuffer)
91 | /// to convert UIImage to CVPixelBuffer
92 | ///
93 | /// - Returns: new [CVPixelBuffer](apple-reference-documentation://hsVf8OXaJX)
94 | func pixelBuffer() -> CVPixelBuffer? {
95 | let attrs = [
96 | kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
97 | kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue
98 | ] as CFDictionary
99 | var pixelBufferOpt: CVPixelBuffer?
100 | let status = CVPixelBufferCreate(
101 | kCFAllocatorDefault,
102 | Int(self.size.width),
103 | Int(self.size.height),
104 | kCVPixelFormatType_32ARGB,
105 | attrs,
106 | &pixelBufferOpt
107 | )
108 | guard status == kCVReturnSuccess, let pixelBuffer = pixelBufferOpt else {
109 | return nil
110 | }
111 |
112 | CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
113 | let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer)
114 |
115 | let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
116 | guard let context = CGContext(
117 | data: pixelData,
118 | width: Int(self.size.width),
119 | height: Int(self.size.height),
120 | bitsPerComponent: 8,
121 | bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer),
122 | space: rgbColorSpace,
123 | bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
124 | ) else {
125 | return nil
126 | }
127 |
128 | context.translateBy(x: 0, y: self.size.height)
129 | context.scaleBy(x: 1.0, y: -1.0)
130 |
131 | UIGraphicsPushContext(context)
132 | self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
133 | UIGraphicsPopContext()
134 | CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
135 |
136 | return pixelBuffer
137 | }
138 |
139 | /// Creates a UIImage from the specified CIImage.
140 | static func from(ciImage: CIImage) -> UIImage {
141 | if let cgImage = CIContext(options: nil).createCGImage(ciImage, from: ciImage.extent) {
142 | return UIImage(cgImage: cgImage)
143 | } else {
144 | return UIImage(ciImage: ciImage, scale: 1.0, orientation: .up)
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/Sources/WeScan/ImageScannerController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageScannerController.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/12/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import UIKit
11 |
12 | /// A set of methods that your delegate object must implement to interact with the image scanner interface.
13 | public protocol ImageScannerControllerDelegate: NSObjectProtocol {
14 |
15 | /// Tells the delegate that the user scanned a document.
16 | ///
17 | /// - Parameters:
18 | /// - scanner: The scanner controller object managing the scanning interface.
19 | /// - results: The results of the user scanning with the camera.
20 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller.
21 | func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults)
22 |
23 | /// Tells the delegate that the user cancelled the scan operation.
24 | ///
25 | /// - Parameters:
26 | /// - scanner: The scanner controller object managing the scanning interface.
27 | /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller.
28 | func imageScannerControllerDidCancel(_ scanner: ImageScannerController)
29 |
30 | /// Tells the delegate that an error occurred during the user's scanning experience.
31 | ///
32 | /// - Parameters:
33 | /// - scanner: The scanner controller object managing the scanning interface.
34 | /// - error: The error that occurred.
35 | func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error)
36 | }
37 |
38 | /// A view controller that manages the full flow for scanning documents.
39 | /// The `ImageScannerController` class is meant to be presented. It consists of a series of 3 different screens which guide the user:
40 | /// 1. Uses the camera to capture an image with a rectangle that has been detected.
41 | /// 2. Edit the detected rectangle.
42 | /// 3. Review the cropped down version of the rectangle.
43 | public final class ImageScannerController: UINavigationController {
44 |
45 | /// The object that acts as the delegate of the `ImageScannerController`.
46 | public weak var imageScannerDelegate: ImageScannerControllerDelegate?
47 |
48 | // MARK: - Life Cycle
49 |
50 | /// A black UIView, used to quickly display a black screen when the shutter button is presseed.
51 | internal let blackFlashView: UIView = {
52 | let view = UIView()
53 | view.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
54 | view.isHidden = true
55 | view.translatesAutoresizingMaskIntoConstraints = false
56 | return view
57 | }()
58 |
59 | override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
60 | return .portrait
61 | }
62 |
63 | public required init(image: UIImage? = nil, delegate: ImageScannerControllerDelegate? = nil) {
64 | super.init(rootViewController: ScannerViewController())
65 |
66 | self.imageScannerDelegate = delegate
67 |
68 | if #available(iOS 13.0, *) {
69 | navigationBar.tintColor = .label
70 | } else {
71 | navigationBar.tintColor = .black
72 | }
73 | navigationBar.isTranslucent = false
74 | self.view.addSubview(blackFlashView)
75 | setupConstraints()
76 |
77 | // If an image was passed in by the host app (e.g. picked from the photo library), use it instead of the document scanner.
78 | if let image {
79 | detect(image: image) { [weak self] detectedQuad in
80 | guard let self else { return }
81 | let editViewController = EditScanViewController(image: image, quad: detectedQuad, rotateImage: false)
82 | self.setViewControllers([editViewController], animated: false)
83 | }
84 | }
85 | }
86 |
87 | override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
88 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
89 | }
90 |
91 | public required init?(coder aDecoder: NSCoder) {
92 | fatalError("init(coder:) has not been implemented")
93 | }
94 |
95 | private func detect(image: UIImage, completion: @escaping (Quadrilateral?) -> Void) {
96 | // Whether or not we detect a quad, present the edit view controller after attempting to detect a quad.
97 | // *** Vision *requires* a completion block to detect rectangles, but it's instant.
98 | // *** When using Vision, we'll present the normal edit view controller first, then present the updated edit view controller later.
99 |
100 | guard let ciImage = CIImage(image: image) else { return }
101 | let orientation = CGImagePropertyOrientation(image.imageOrientation)
102 | let orientedImage = ciImage.oriented(forExifOrientation: Int32(orientation.rawValue))
103 |
104 | if #available(iOS 11.0, *) {
105 | // Use the VisionRectangleDetector on iOS 11 to attempt to find a rectangle from the initial image.
106 | VisionRectangleDetector.rectangle(forImage: ciImage, orientation: orientation) { quad in
107 | let detectedQuad = quad?.toCartesian(withHeight: orientedImage.extent.height)
108 | completion(detectedQuad)
109 | }
110 | } else {
111 | // Use the CIRectangleDetector on iOS 10 to attempt to find a rectangle from the initial image.
112 | let detectedQuad = CIRectangleDetector.rectangle(forImage: ciImage)?.toCartesian(withHeight: orientedImage.extent.height)
113 | completion(detectedQuad)
114 | }
115 | }
116 |
117 | public func useImage(image: UIImage) {
118 | guard topViewController is ScannerViewController else { return }
119 |
120 | detect(image: image) { [weak self] detectedQuad in
121 | guard let self else { return }
122 | let editViewController = EditScanViewController(image: image, quad: detectedQuad, rotateImage: false)
123 | self.setViewControllers([editViewController], animated: true)
124 | }
125 | }
126 |
127 | public func resetScanner() {
128 | setViewControllers([ScannerViewController()], animated: true)
129 | }
130 |
131 | private func setupConstraints() {
132 | let blackFlashViewConstraints = [
133 | blackFlashView.topAnchor.constraint(equalTo: view.topAnchor),
134 | blackFlashView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
135 | view.bottomAnchor.constraint(equalTo: blackFlashView.bottomAnchor),
136 | view.trailingAnchor.constraint(equalTo: blackFlashView.trailingAnchor)
137 | ]
138 |
139 | NSLayoutConstraint.activate(blackFlashViewConstraints)
140 | }
141 |
142 | internal func flashToBlack() {
143 | view.bringSubviewToFront(blackFlashView)
144 | blackFlashView.isHidden = false
145 | let flashDuration = DispatchTime.now() + 0.05
146 | DispatchQueue.main.asyncAfter(deadline: flashDuration) {
147 | self.blackFlashView.isHidden = true
148 | }
149 | }
150 | }
151 |
152 | /// Data structure containing information about a scan, including both the image and an optional PDF.
153 | public struct ImageScannerScan {
154 | public enum ImageScannerError: Error {
155 | case failedToGeneratePDF
156 | }
157 |
158 | public var image: UIImage
159 |
160 | public func generatePDFData(completion: @escaping (Result) -> Void) {
161 | DispatchQueue.global(qos: .userInteractive).async {
162 | if let pdfData = self.image.pdfData() {
163 | completion(.success(pdfData))
164 | } else {
165 | completion(.failure(.failedToGeneratePDF))
166 | }
167 | }
168 |
169 | }
170 |
171 | mutating func rotate(by rotationAngle: Measurement) {
172 | guard rotationAngle.value != 0, rotationAngle.value != 360 else { return }
173 | image = image.rotated(by: rotationAngle) ?? image
174 | }
175 | }
176 |
177 | /// Data structure containing information about a scanning session.
178 | /// Includes the original scan, cropped scan, detected rectangle, and whether the user selected the enhanced scan.
179 | /// May also include an enhanced scan if no errors were encountered.
180 | public struct ImageScannerResults {
181 |
182 | /// The original scan taken by the user, prior to the cropping applied by WeScan.
183 | public var originalScan: ImageScannerScan
184 |
185 | /// The deskewed and cropped scan using the detected rectangle, without any filters.
186 | public var croppedScan: ImageScannerScan
187 |
188 | /// The enhanced scan, passed through an Adaptive Thresholding function.
189 | /// This image will always be grayscale and may not always be available.
190 | public var enhancedScan: ImageScannerScan?
191 |
192 | /// Whether the user selected the enhanced scan or not.
193 | /// The `enhancedScan` may still be available even if it has not been selected by the user.
194 | public var doesUserPreferEnhancedScan: Bool
195 |
196 | /// The detected rectangle which was used to generate the `scannedImage`.
197 | public var detectedRectangle: Quadrilateral
198 |
199 | init(
200 | detectedRectangle: Quadrilateral,
201 | originalScan: ImageScannerScan,
202 | croppedScan: ImageScannerScan,
203 | enhancedScan: ImageScannerScan?,
204 | doesUserPreferEnhancedScan: Bool = false
205 | ) {
206 | self.detectedRectangle = detectedRectangle
207 |
208 | self.originalScan = originalScan
209 | self.croppedScan = croppedScan
210 | self.enhancedScan = enhancedScan
211 |
212 | self.doesUserPreferEnhancedScan = doesUserPreferEnhancedScan
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/Sources/WeScan/Protocols/CaptureDevice.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureDevice.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 28/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import Foundation
11 |
12 | protocol CaptureDevice: AnyObject {
13 | var torchMode: AVCaptureDevice.TorchMode { get set }
14 | var isTorchAvailable: Bool { get }
15 |
16 | var focusMode: AVCaptureDevice.FocusMode { get set }
17 | var focusPointOfInterest: CGPoint { get set }
18 | var isFocusPointOfInterestSupported: Bool { get }
19 |
20 | var exposureMode: AVCaptureDevice.ExposureMode { get set }
21 | var exposurePointOfInterest: CGPoint { get set }
22 | var isExposurePointOfInterestSupported: Bool { get }
23 |
24 | var isSubjectAreaChangeMonitoringEnabled: Bool { get set }
25 |
26 | func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool
27 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool
28 | func unlockForConfiguration()
29 | func lockForConfiguration() throws
30 | }
31 |
32 | extension AVCaptureDevice: CaptureDevice { }
33 |
34 | final class MockCaptureDevice: CaptureDevice {
35 | var torchMode: AVCaptureDevice.TorchMode = .off
36 | var isTorchAvailable = true
37 |
38 | var focusMode: AVCaptureDevice.FocusMode = .continuousAutoFocus
39 | var focusPointOfInterest: CGPoint = .zero
40 | var isFocusPointOfInterestSupported = true
41 |
42 | var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure
43 | var exposurePointOfInterest: CGPoint = .zero
44 | var isExposurePointOfInterestSupported = true
45 | var isSubjectAreaChangeMonitoringEnabled = false
46 |
47 | func unlockForConfiguration() {
48 | return
49 | }
50 |
51 | func lockForConfiguration() throws {
52 | return
53 | }
54 |
55 | func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool {
56 | return true
57 | }
58 |
59 | func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool {
60 | return true
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/WeScan/Protocols/Transformable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extendable.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/15/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// Objects that conform to the Transformable protocol are capable of being transformed with a `CGAffineTransform`.
13 | protocol Transformable {
14 |
15 | /// Applies the given `CGAffineTransform`.
16 | ///
17 | /// - Parameters:
18 | /// - t: The transform to apply
19 | /// - Returns: The same object transformed by the passed in `CGAffineTransform`.
20 | func applying(_ transform: CGAffineTransform) -> Self
21 |
22 | }
23 |
24 | extension Transformable {
25 |
26 | /// Applies multiple given transforms in the given order.
27 | ///
28 | /// - Parameters:
29 | /// - transforms: The transforms to apply.
30 | /// - Returns: The same object transformed by the passed in `CGAffineTransform`s.
31 | func applyTransforms(_ transforms: [CGAffineTransform]) -> Self {
32 |
33 | var transformableObject = self
34 |
35 | transforms.forEach { transform in
36 | transformableObject = transformableObject.applying(transform)
37 | }
38 |
39 | return transformableObject
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/enhance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/enhance.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/enhance@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/enhance@2x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/enhance@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/enhance@3x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flash.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flash@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flash@2x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flash@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flash@3x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flashUnavailable.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flashUnavailable.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flashUnavailable@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flashUnavailable@2x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/flashUnavailable@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/flashUnavailable@3x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/rotate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/rotate.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/rotate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/rotate@2x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Assets/rotate@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Sources/WeScan/Resources/Assets/rotate@3x.png
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/ar.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "التالي";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "تعديل الصورة";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "عرض";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "إلغاء";
20 | "wescan.scanning.auto" = "تلقائي";
21 | "wescan.scanning.manual" = "يدوي";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/cs.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Martin Georgiu on 3/8/20.
6 | Copyright © 2020 Martin Georgiu. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Pokračovat";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Upravit";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Náhled";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Zpět";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Ručně";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/de.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Weiter";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Scan editieren";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Überprüfen";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Abbrechen";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manuell";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/en.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Next";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Edit Scan";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Review";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Cancel";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manual";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/es-419.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Nicolas Garcia on 3/9/20.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Siguiente";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Editar Escaneo";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Revisión";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Cancelar";
20 | "wescan.scanning.auto" = "Automático";
21 | "wescan.scanning.manual" = "Manual";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/es.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Nicolas Garcia on 3/9/20.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Siguiente";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Editar Escaneo";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Revisión";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Cancelar";
20 | "wescan.scanning.auto" = "Automático";
21 | "wescan.scanning.manual" = "Manual";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/fr.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Suivant";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Modifier";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Aperçu";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Annuler";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manuel";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/hu.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | Localizable.strings
3 | WeScan
4 |
5 | Created by Rénes Péter on 2019. 07. 08..
6 | Copyright © 2019. WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Következő";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Szerkesztés";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Előnézet";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Mégsem";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Kézi";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/it.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Avanti";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Modifica Scansione";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Analisi";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Annulla";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manuale";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/ko.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "다음";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "스캔 수정";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "검토";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "취소";
20 | "wescan.scanning.auto" = "자동";
21 | "wescan.scanning.manual" = "수동";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/nl.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Volgende";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Wijzig Scan";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Beoordelen";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Annuleren";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Handmatig";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/pl.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Lukasz Szarkowicz on 06/03/2020.
6 | Copyright © 2020 Lukasz Szarkowicz. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Dalej";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Edytuj dokument";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Podgląd";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Anuluj";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manualnie";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/pt-BR.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Avançar";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Editar imagem";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Revisar";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Cancelar";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manual";
22 |
23 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/pt-PT.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Avançar";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Editar imagem";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Rever";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Cancelar";
20 | "wescan.scanning.auto" = "Auto";
21 | "wescan.scanning.manual" = "Manual";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/ru.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Dmitriy Toropkin on 4/16/20.
6 | Copyright © 2020 Dmitriy Toropkin. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Продолжить";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Редактирование";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Предпросмотр";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Отмена";
20 | "wescan.scanning.auto" = "Авто";
21 | "wescan.scanning.manual" = "Ручное";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/sv.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Ola Nilsson on 12/8/20.
6 | Copyright © 2020 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Nästa";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Redigera";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "Granska";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Avbryt";
20 | "wescan.scanning.auto" = "Automatisk";
21 | "wescan.scanning.manual" = "Manuell";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/tr.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Hakan Eren on 30/04/2020.
6 | Copyright © 2020 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "Sonraki";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "Düzenle";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "İncele";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "Vazgeç";
20 | "wescan.scanning.auto" = "Otomatik";
21 | "wescan.scanning.manual" = "Manuel";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/zh-Hans.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "下一步";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "编辑";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "回顾";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "取消";
20 | "wescan.scanning.auto" = "自动";
21 | "wescan.scanning.manual" = "手动";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Resources/Localisation/zh-Hant.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "下一步";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "編輯";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "回顧";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "取消";
20 | "wescan.scanning.auto" = "自動";
21 | "wescan.scanning.manual" = "手動";
22 |
--------------------------------------------------------------------------------
/Sources/WeScan/Review/ReviewViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewViewController.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/25/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// The `ReviewViewController` offers an interface to review the image after it
12 | /// has been cropped and deskewed according to the passed in quadrilateral.
13 | final class ReviewViewController: UIViewController {
14 |
15 | private var rotationAngle = Measurement(value: 0, unit: .degrees)
16 | private var enhancedImageIsAvailable = false
17 | private var isCurrentlyDisplayingEnhancedImage = false
18 |
19 | lazy var imageView: UIImageView = {
20 | let imageView = UIImageView()
21 | imageView.clipsToBounds = true
22 | imageView.isOpaque = true
23 | imageView.image = results.croppedScan.image
24 | imageView.backgroundColor = .black
25 | imageView.contentMode = .scaleAspectFit
26 | imageView.translatesAutoresizingMaskIntoConstraints = false
27 | return imageView
28 | }()
29 |
30 | private lazy var enhanceButton: UIBarButtonItem = {
31 | let image = UIImage(
32 | systemName: "wand.and.rays.inverse",
33 | named: "enhance",
34 | in: Bundle(for: ScannerViewController.self),
35 | compatibleWith: nil
36 | )
37 | let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(toggleEnhancedImage))
38 | button.tintColor = .white
39 | return button
40 | }()
41 |
42 | private lazy var rotateButton: UIBarButtonItem = {
43 | let image = UIImage(systemName: "rotate.right", named: "rotate", in: Bundle(for: ScannerViewController.self), compatibleWith: nil)
44 | let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(rotateImage))
45 | button.tintColor = .white
46 | return button
47 | }()
48 |
49 | private lazy var doneButton: UIBarButtonItem = {
50 | let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(finishScan))
51 | button.tintColor = navigationController?.navigationBar.tintColor
52 | return button
53 | }()
54 |
55 | private let results: ImageScannerResults
56 |
57 | // MARK: - Life Cycle
58 |
59 | init(results: ImageScannerResults) {
60 | self.results = results
61 | super.init(nibName: nil, bundle: nil)
62 | }
63 |
64 | required init?(coder aDecoder: NSCoder) {
65 | fatalError("init(coder:) has not been implemented")
66 | }
67 |
68 | override func viewDidLoad() {
69 | super.viewDidLoad()
70 |
71 | enhancedImageIsAvailable = results.enhancedScan != nil
72 |
73 | setupViews()
74 | setupToolbar()
75 | setupConstraints()
76 |
77 | title = NSLocalizedString("wescan.review.title",
78 | tableName: nil,
79 | bundle: Bundle(for: ReviewViewController.self),
80 | value: "Review",
81 | comment: "The review title of the ReviewController"
82 | )
83 | navigationItem.rightBarButtonItem = doneButton
84 | }
85 |
86 | override func viewWillAppear(_ animated: Bool) {
87 | super.viewWillAppear(animated)
88 |
89 | // We only show the toolbar (with the enhance button) if the enhanced image is available.
90 | if enhancedImageIsAvailable {
91 | navigationController?.setToolbarHidden(false, animated: true)
92 | }
93 | }
94 |
95 | override func viewWillDisappear(_ animated: Bool) {
96 | super.viewWillDisappear(animated)
97 | navigationController?.setToolbarHidden(true, animated: true)
98 | }
99 |
100 | // MARK: Setups
101 |
102 | private func setupViews() {
103 | view.addSubview(imageView)
104 | }
105 |
106 | private func setupToolbar() {
107 | guard enhancedImageIsAvailable else { return }
108 |
109 | navigationController?.toolbar.barStyle = .blackTranslucent
110 |
111 | let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
112 | let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
113 | toolbarItems = [fixedSpace, enhanceButton, flexibleSpace, rotateButton, fixedSpace]
114 | }
115 |
116 | private func setupConstraints() {
117 | imageView.translatesAutoresizingMaskIntoConstraints = false
118 |
119 | var imageViewConstraints: [NSLayoutConstraint] = []
120 | if #available(iOS 11.0, *) {
121 | imageViewConstraints = [
122 | view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.topAnchor),
123 | view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor),
124 | view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor),
125 | view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor)
126 | ]
127 | } else {
128 | imageViewConstraints = [
129 | view.topAnchor.constraint(equalTo: imageView.topAnchor),
130 | view.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
131 | view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
132 | view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor)
133 | ]
134 | }
135 |
136 | NSLayoutConstraint.activate(imageViewConstraints)
137 | }
138 |
139 | // MARK: - Actions
140 |
141 | @objc private func reloadImage() {
142 | if enhancedImageIsAvailable, isCurrentlyDisplayingEnhancedImage {
143 | imageView.image = results.enhancedScan?.image.rotated(by: rotationAngle) ?? results.enhancedScan?.image
144 | } else {
145 | imageView.image = results.croppedScan.image.rotated(by: rotationAngle) ?? results.croppedScan.image
146 | }
147 | }
148 |
149 | @objc func toggleEnhancedImage() {
150 | guard enhancedImageIsAvailable else { return }
151 |
152 | isCurrentlyDisplayingEnhancedImage.toggle()
153 | reloadImage()
154 |
155 | if isCurrentlyDisplayingEnhancedImage {
156 | enhanceButton.tintColor = .yellow
157 | } else {
158 | enhanceButton.tintColor = .white
159 | }
160 | }
161 |
162 | @objc func rotateImage() {
163 | rotationAngle.value += 90
164 |
165 | if rotationAngle.value == 360 {
166 | rotationAngle.value = 0
167 | }
168 |
169 | reloadImage()
170 | }
171 |
172 | @objc private func finishScan() {
173 | guard let imageScannerController = navigationController as? ImageScannerController else { return }
174 |
175 | var newResults = results
176 | newResults.croppedScan.rotate(by: rotationAngle)
177 | newResults.enhancedScan?.rotate(by: rotationAngle)
178 | newResults.doesUserPreferEnhancedScan = isCurrentlyDisplayingEnhancedImage
179 | imageScannerController.imageScannerDelegate?
180 | .imageScannerController(imageScannerController, didFinishScanningWithResults: newResults)
181 | }
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/Sources/WeScan/Scan/CameraScannerViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CameraScannerViewController.swift
3 | // WeScan
4 | //
5 | // Created by Chawatvish Worrapoj on 6/1/2020
6 | // Copyright © 2020 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import UIKit
11 |
12 | /// A set of methods that your delegate object must implement to get capture image.
13 | /// If camera module doesn't work it will send error back to your delegate object.
14 | public protocol CameraScannerViewOutputDelegate: AnyObject {
15 | func captureImageFailWithError(error: Error)
16 | func captureImageSuccess(image: UIImage, withQuad quad: Quadrilateral?)
17 | }
18 |
19 | /// A view controller that manages the camera module and auto capture of rectangle shape of document
20 | /// The `CameraScannerViewController` class is individual camera view include touch for focus, flash control,
21 | /// capture control and auto detect rectangle shape of object.
22 | public final class CameraScannerViewController: UIViewController {
23 |
24 | /// The status of auto scan.
25 | public var isAutoScanEnabled: Bool = CaptureSession.current.isAutoScanEnabled {
26 | didSet {
27 | CaptureSession.current.isAutoScanEnabled = isAutoScanEnabled
28 | }
29 | }
30 |
31 | /// The callback to caller view to send back success or fail.
32 | public weak var delegate: CameraScannerViewOutputDelegate?
33 |
34 | private var captureSessionManager: CaptureSessionManager?
35 | private let videoPreviewLayer = AVCaptureVideoPreviewLayer()
36 |
37 | /// The view that shows the focus rectangle (when the user taps to focus, similar to the Camera app)
38 | private var focusRectangle: FocusRectangleView!
39 |
40 | /// The view that draws the detected rectangles.
41 | private let quadView = QuadrilateralView()
42 |
43 | /// Whether flash is enabled
44 | private var flashEnabled = false
45 |
46 | override public func viewDidLoad() {
47 | super.viewDidLoad()
48 | setupView()
49 | }
50 |
51 | override public func viewWillAppear(_ animated: Bool) {
52 | super.viewWillAppear(animated)
53 | CaptureSession.current.isEditing = false
54 | quadView.removeQuadrilateral()
55 | captureSessionManager?.start()
56 | UIApplication.shared.isIdleTimerDisabled = true
57 | }
58 |
59 | override public func viewDidLayoutSubviews() {
60 | super.viewDidLayoutSubviews()
61 |
62 | videoPreviewLayer.frame = view.layer.bounds
63 | }
64 |
65 | override public func viewWillDisappear(_ animated: Bool) {
66 | super.viewWillDisappear(animated)
67 | UIApplication.shared.isIdleTimerDisabled = false
68 | captureSessionManager?.stop()
69 | guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return }
70 | if device.torchMode == .on {
71 | toggleFlash()
72 | }
73 | }
74 |
75 | private func setupView() {
76 | view.backgroundColor = .darkGray
77 | view.layer.addSublayer(videoPreviewLayer)
78 | quadView.translatesAutoresizingMaskIntoConstraints = false
79 | quadView.editable = false
80 | view.addSubview(quadView)
81 | setupConstraints()
82 |
83 | captureSessionManager = CaptureSessionManager(videoPreviewLayer: videoPreviewLayer)
84 | captureSessionManager?.delegate = self
85 |
86 | NotificationCenter.default.addObserver(
87 | self,
88 | selector: #selector(subjectAreaDidChange),
89 | name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange,
90 | object: nil
91 | )
92 | }
93 |
94 | private func setupConstraints() {
95 | var quadViewConstraints = [NSLayoutConstraint]()
96 |
97 | quadViewConstraints = [
98 | quadView.topAnchor.constraint(equalTo: view.topAnchor),
99 | view.bottomAnchor.constraint(equalTo: quadView.bottomAnchor),
100 | view.trailingAnchor.constraint(equalTo: quadView.trailingAnchor),
101 | quadView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
102 | ]
103 | NSLayoutConstraint.activate(quadViewConstraints)
104 | }
105 |
106 | /// Called when the AVCaptureDevice detects that the subject area has changed significantly. When it's called,
107 | /// we reset the focus so the camera is no longer out of focus.
108 | @objc private func subjectAreaDidChange() {
109 | /// Reset the focus and exposure back to automatic
110 | do {
111 | try CaptureSession.current.resetFocusToAuto()
112 | } catch {
113 | let error = ImageScannerControllerError.inputDevice
114 | guard let captureSessionManager else { return }
115 | captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error)
116 | return
117 | }
118 |
119 | /// Remove the focus rectangle if one exists
120 | CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: true)
121 | }
122 |
123 | override public func touchesBegan(_ touches: Set, with event: UIEvent?) {
124 | super.touchesBegan(touches, with: event)
125 |
126 | guard let touch = touches.first else { return }
127 | let touchPoint = touch.location(in: view)
128 | let convertedTouchPoint: CGPoint = videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint)
129 |
130 | CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: false)
131 |
132 | focusRectangle = FocusRectangleView(touchPoint: touchPoint)
133 | focusRectangle.setBorder(color: UIColor.white.cgColor)
134 | view.addSubview(focusRectangle)
135 |
136 | do {
137 | try CaptureSession.current.setFocusPointToTapPoint(convertedTouchPoint)
138 | } catch {
139 | let error = ImageScannerControllerError.inputDevice
140 | guard let captureSessionManager else { return }
141 | captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error)
142 | return
143 | }
144 | }
145 |
146 | public func capture() {
147 | captureSessionManager?.capturePhoto()
148 | }
149 |
150 | public func toggleFlash() {
151 | let state = CaptureSession.current.toggleFlash()
152 | switch state {
153 | case .on:
154 | flashEnabled = true
155 | case .off:
156 | flashEnabled = false
157 | case .unknown, .unavailable:
158 | flashEnabled = false
159 | }
160 | }
161 |
162 | public func toggleAutoScan() {
163 | isAutoScanEnabled.toggle()
164 | }
165 | }
166 |
167 | extension CameraScannerViewController: RectangleDetectionDelegateProtocol {
168 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error) {
169 | delegate?.captureImageFailWithError(error: error)
170 | }
171 |
172 | func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager) {
173 | captureSessionManager.stop()
174 | }
175 |
176 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager,
177 | didCapturePicture picture: UIImage,
178 | withQuad quad: Quadrilateral?) {
179 | delegate?.captureImageSuccess(image: picture, withQuad: quad)
180 | }
181 |
182 | func captureSessionManager(_ captureSessionManager: CaptureSessionManager,
183 | didDetectQuad quad: Quadrilateral?,
184 | _ imageSize: CGSize) {
185 | guard let quad else {
186 | // If no quad has been detected, we remove the currently displayed on on the quadView.
187 | quadView.removeQuadrilateral()
188 | return
189 | }
190 |
191 | let portraitImageSize = CGSize(width: imageSize.height, height: imageSize.width)
192 | let scaleTransform = CGAffineTransform.scaleTransform(forSize: portraitImageSize, aspectFillInSize: quadView.bounds.size)
193 | let scaledImageSize = imageSize.applying(scaleTransform)
194 | let rotationTransform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
195 | let imageBounds = CGRect(origin: .zero, size: scaledImageSize).applying(rotationTransform)
196 | let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: imageBounds, toCenterOfRect: quadView.bounds)
197 | let transforms = [scaleTransform, rotationTransform, translationTransform]
198 | let transformedQuad = quad.applyTransforms(transforms)
199 | quadView.drawQuadrilateral(quad: transformedQuad, animated: true)
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/Sources/WeScan/Scan/FocusRectangleView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusRectangleView.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 16/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A yellow rectangle used to display the last 'tap to focus' point
12 | final class FocusRectangleView: UIView {
13 | convenience init(touchPoint: CGPoint) {
14 | let originalSize: CGFloat = 200
15 | let finalSize: CGFloat = 80
16 |
17 | // Here, we create the frame to be the `originalSize`, with it's center being the `touchPoint`.
18 | self.init(
19 | frame: CGRect(
20 | x: touchPoint.x - (originalSize / 2),
21 | y: touchPoint.y - (originalSize / 2),
22 | width: originalSize,
23 | height: originalSize
24 | )
25 | )
26 |
27 | backgroundColor = .clear
28 | layer.borderWidth = 2.0
29 | layer.cornerRadius = 6.0
30 | layer.borderColor = UIColor.yellow.cgColor
31 |
32 | // Here, we animate the rectangle from the `originalSize` to the `finalSize` by calculating the difference.
33 | UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: {
34 | self.frame.origin.x += (originalSize - finalSize) / 2
35 | self.frame.origin.y += (originalSize - finalSize) / 2
36 |
37 | self.frame.size.width -= (originalSize - finalSize)
38 | self.frame.size.height -= (originalSize - finalSize)
39 | })
40 | }
41 |
42 | public func setBorder(color: CGColor) {
43 | layer.borderColor = color
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/WeScan/Scan/ShutterButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShutterButton.swift
3 | // WeScan
4 | //
5 | // Created by Boris Emorine on 2/26/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | /// A simple button used for the shutter.
12 | final class ShutterButton: UIControl {
13 |
14 | private let outerRingLayer = CAShapeLayer()
15 | private let innerCircleLayer = CAShapeLayer()
16 |
17 | private let outerRingRatio: CGFloat = 0.80
18 | private let innerRingRatio: CGFloat = 0.75
19 |
20 | private let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
21 |
22 | override var isHighlighted: Bool {
23 | didSet {
24 | if oldValue != isHighlighted {
25 | animateInnerCircleLayer(forHighlightedState: isHighlighted)
26 | }
27 | }
28 | }
29 |
30 | // MARK: - Life Cycle
31 |
32 | override init(frame: CGRect) {
33 | super.init(frame: frame)
34 | layer.addSublayer(outerRingLayer)
35 | layer.addSublayer(innerCircleLayer)
36 | backgroundColor = .clear
37 | isAccessibilityElement = true
38 | accessibilityTraits = UIAccessibilityTraits.button
39 | impactFeedbackGenerator.prepare()
40 | }
41 |
42 | required init?(coder aDecoder: NSCoder) {
43 | fatalError("init(coder:) has not been implemented")
44 | }
45 |
46 | // MARK: - Drawing
47 |
48 | override func draw(_ rect: CGRect) {
49 | super.draw(rect)
50 |
51 | outerRingLayer.frame = rect
52 | outerRingLayer.path = pathForOuterRing(inRect: rect).cgPath
53 | outerRingLayer.fillColor = UIColor.white.cgColor
54 | outerRingLayer.rasterizationScale = UIScreen.main.scale
55 | outerRingLayer.shouldRasterize = true
56 |
57 | innerCircleLayer.frame = rect
58 | innerCircleLayer.path = pathForInnerCircle(inRect: rect).cgPath
59 | innerCircleLayer.fillColor = UIColor.white.cgColor
60 | innerCircleLayer.rasterizationScale = UIScreen.main.scale
61 | innerCircleLayer.shouldRasterize = true
62 | }
63 |
64 | // MARK: - Animation
65 |
66 | private func animateInnerCircleLayer(forHighlightedState isHighlighted: Bool) {
67 | let animation = CAKeyframeAnimation(keyPath: "transform")
68 | var values = [
69 | CATransform3DMakeScale(1.0, 1.0, 1.0),
70 | CATransform3DMakeScale(0.9, 0.9, 0.9),
71 | CATransform3DMakeScale(0.93, 0.93, 0.93),
72 | CATransform3DMakeScale(0.9, 0.9, 0.9)
73 | ]
74 | if isHighlighted == false {
75 | values = [CATransform3DMakeScale(0.9, 0.9, 0.9), CATransform3DMakeScale(1.0, 1.0, 1.0)]
76 | }
77 | animation.values = values
78 | animation.isRemovedOnCompletion = false
79 | animation.fillMode = CAMediaTimingFillMode.forwards
80 | animation.duration = isHighlighted ? 0.35 : 0.10
81 |
82 | innerCircleLayer.add(animation, forKey: "transform")
83 | impactFeedbackGenerator.impactOccurred()
84 | }
85 |
86 | // MARK: - Paths
87 |
88 | private func pathForOuterRing(inRect rect: CGRect) -> UIBezierPath {
89 | let path = UIBezierPath(ovalIn: rect)
90 |
91 | let innerRect = rect.scaleAndCenter(withRatio: outerRingRatio)
92 | let innerPath = UIBezierPath(ovalIn: innerRect).reversing()
93 |
94 | path.append(innerPath)
95 |
96 | return path
97 | }
98 |
99 | private func pathForInnerCircle(inRect rect: CGRect) -> UIBezierPath {
100 | let rect = rect.scaleAndCenter(withRatio: innerRingRatio)
101 | let path = UIBezierPath(ovalIn: rect)
102 |
103 | return path
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Sources/WeScan/Session/CaptureSession+Flash.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession+Flash.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 28/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Extension to CaptureSession to manage the device flashlight
12 | extension CaptureSession {
13 | /// The possible states that the current device's flashlight can be in
14 | enum FlashState {
15 | case on
16 | case off
17 | case unavailable
18 | case unknown
19 | }
20 |
21 | /// Toggles the current device's flashlight on or off.
22 | func toggleFlash() -> FlashState {
23 | guard let device, device.isTorchAvailable else { return .unavailable }
24 |
25 | do {
26 | try device.lockForConfiguration()
27 | } catch {
28 | return .unknown
29 | }
30 |
31 | defer {
32 | device.unlockForConfiguration()
33 | }
34 |
35 | if device.torchMode == .on {
36 | device.torchMode = .off
37 | return .off
38 | } else if device.torchMode == .off {
39 | device.torchMode = .on
40 | return .on
41 | }
42 |
43 | return .unknown
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/WeScan/Session/CaptureSession+Focus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession+Focus.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 28/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 |
12 | /// Extension to CaptureSession that controls auto focus
13 | extension CaptureSession {
14 | /// Sets the camera's exposure and focus point to the given point
15 | func setFocusPointToTapPoint(_ tapPoint: CGPoint) throws {
16 | guard let device else {
17 | let error = ImageScannerControllerError.inputDevice
18 | throw error
19 | }
20 |
21 | try device.lockForConfiguration()
22 |
23 | defer {
24 | device.unlockForConfiguration()
25 | }
26 |
27 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) {
28 | device.focusPointOfInterest = tapPoint
29 | device.focusMode = .autoFocus
30 | }
31 |
32 | if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
33 | device.exposurePointOfInterest = tapPoint
34 | device.exposureMode = .continuousAutoExposure
35 | }
36 | }
37 |
38 | /// Resets the camera's exposure and focus point to automatic
39 | func resetFocusToAuto() throws {
40 | guard let device else {
41 | let error = ImageScannerControllerError.inputDevice
42 | throw error
43 | }
44 |
45 | try device.lockForConfiguration()
46 |
47 | defer {
48 | device.unlockForConfiguration()
49 | }
50 |
51 | if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) {
52 | device.focusMode = .continuousAutoFocus
53 | }
54 |
55 | if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) {
56 | device.exposureMode = .continuousAutoExposure
57 | }
58 | }
59 |
60 | /// Removes an existing focus rectangle if one exists, optionally animating the exit
61 | func removeFocusRectangleIfNeeded(_ focusRectangle: FocusRectangleView?, animated: Bool) {
62 | guard let focusRectangle else { return }
63 | if animated {
64 | UIView.animate(withDuration: 0.3, delay: 1.0, animations: {
65 | focusRectangle.alpha = 0.0
66 | }, completion: { _ in
67 | focusRectangle.removeFromSuperview()
68 | })
69 | } else {
70 | focusRectangle.removeFromSuperview()
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/WeScan/Session/CaptureSession+Orientation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession+Orientation.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 23/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import CoreMotion
10 | import Foundation
11 | import UIKit
12 |
13 | /// Extension to CaptureSession with support for automatically detecting the current orientation via CoreMotion
14 | /// Which works even if the user has enabled portrait lock.
15 | extension CaptureSession {
16 | /// Detect the current orientation of the device with CoreMotion and use it to set the `editImageOrientation`.
17 | func setImageOrientation() {
18 | let motion = CMMotionManager()
19 |
20 | /// This value should be 0.2, but since we only need one cycle (and stop updates immediately),
21 | /// we set it low to get the orientation immediately
22 | motion.accelerometerUpdateInterval = 0.01
23 |
24 | guard motion.isAccelerometerAvailable else { return }
25 |
26 | motion.startAccelerometerUpdates(to: OperationQueue()) { data, error in
27 | guard let data, error == nil else { return }
28 |
29 | /// The minimum amount of sensitivity for the landscape orientations
30 | /// This is to prevent the landscape orientation being incorrectly used
31 | /// Higher = easier for landscape to be detected, lower = easier for portrait to be detected
32 | let motionThreshold = 0.35
33 |
34 | if data.acceleration.x >= motionThreshold {
35 | self.editImageOrientation = .left
36 | } else if data.acceleration.x <= -motionThreshold {
37 | self.editImageOrientation = .right
38 | } else {
39 | /// This means the device is either in the 'up' or 'down' orientation, BUT,
40 | /// it's very rare for someone to be using their phone upside down, so we use 'up' all the time
41 | /// Which prevents accidentally making the document be scanned upside down
42 | self.editImageOrientation = .up
43 | }
44 |
45 | motion.stopAccelerometerUpdates()
46 |
47 | // If the device is reporting a specific landscape orientation, we'll use it over the accelerometer's update.
48 | // We don't use this to check for "portrait" because only the accelerometer works when portrait lock is enabled.
49 | // For some reason, the left/right orientations are incorrect (flipped) :/
50 | switch UIDevice.current.orientation {
51 | case .landscapeLeft:
52 | self.editImageOrientation = .right
53 | case .landscapeRight:
54 | self.editImageOrientation = .left
55 | default:
56 | break
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/WeScan/Session/CaptureSession.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSession.swift
3 | // WeScan
4 | //
5 | // Created by Julian Schiavo on 23/9/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import Foundation
11 |
12 | /// A class containing global variables and settings for this capture session
13 | final class CaptureSession {
14 |
15 | static let current = CaptureSession()
16 |
17 | /// The AVCaptureDevice used for the flash and focus setting
18 | var device: CaptureDevice?
19 |
20 | /// Whether the user is past the scanning screen or not (needed to disable auto scan on other screens)
21 | var isEditing: Bool
22 |
23 | /// The status of auto scan. Auto scan tries to automatically scan a detected rectangle if it has a high enough accuracy.
24 | var isAutoScanEnabled: Bool
25 |
26 | /// The orientation of the captured image
27 | var editImageOrientation: CGImagePropertyOrientation
28 |
29 | private init(isAutoScanEnabled: Bool = true, editImageOrientation: CGImagePropertyOrientation = .up) {
30 | self.device = AVCaptureDevice.default(for: .video)
31 |
32 | self.isEditing = false
33 | self.isAutoScanEnabled = isAutoScanEnabled
34 | self.editImageOrientation = editImageOrientation
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/Sources/WeScan/ja.lproj/Localizable.strings:
--------------------------------------------------------------------------------
1 | /*
2 | localizable.strings
3 | WeScanSampleProject
4 |
5 | Created by Boris Emorine on 2/27/18.
6 | Copyright © 2018 WeTransfer. All rights reserved.
7 | */
8 |
9 | /* The "Next" button on the right side of the navigation bar on the Edit screen. */
10 | "wescan.edit.button.next" = "次";
11 |
12 | /* The title on the navigation bar of the Edit screen. */
13 | "wescan.edit.title" = "スキャン編集";
14 |
15 | /* The title on the navigation bar of the Review screen. */
16 | "wescan.review.title" = "レビュー";
17 |
18 | /* The button titles on the Scanning screen. */
19 | "wescan.scanning.cancel" = "キャンセル";
20 | "wescan.scanning.auto" = "自動";
21 | "wescan.scanning.manual" = "マニュアル";
22 |
23 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/AVCaptureVideoOrientationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVCaptureVideoOrientationTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import XCTest
11 |
12 | @testable import WeScan
13 |
14 | final class AVCaptureVideoOrientationTests: XCTestCase {
15 |
16 | func testPortaitsMapToPortrait() {
17 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .portrait), .portrait)
18 | }
19 |
20 | func testPortaitsUpsideDownMapToPortraitUpsideDown() {
21 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .portraitUpsideDown), .portraitUpsideDown)
22 | }
23 |
24 | func testLandscapeLeftMapToLandscapeLeft() {
25 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .landscapeLeft), .landscapeLeft)
26 | }
27 |
28 | func testLandscapeRightMapToLandscapeRight() {
29 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .landscapeRight), .landscapeRight)
30 | }
31 |
32 | func testFaceUpMapToPortrait() {
33 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .faceUp), .portrait)
34 | }
35 |
36 | func testFaceDownMapToPortraitUpsideDown() {
37 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .faceDown), .portraitUpsideDown)
38 | }
39 |
40 | func testDefaultToPortrait() {
41 | XCTAssertEqual(AVCaptureVideoOrientation(deviceOrientation: .unknown), .portrait)
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/ArrayTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Boris Emorine on 3/6/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class ArrayTests: XCTestCase {
13 |
14 | var funnel = RectangleFeaturesFunnel()
15 |
16 | override func setUp() {
17 | super.setUp()
18 | funnel = RectangleFeaturesFunnel()
19 | }
20 |
21 | func testBiggestRectangle() {
22 | var rects1 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: 10)
23 | var rects2 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect2, withCount: 10)
24 |
25 | var rectangles: [Quadrilateral] = rects1 + rects2
26 |
27 | var biggestRectangle = rectangles.biggest()
28 | XCTAssert(biggestRectangle!.isWithin(1.0, ofRectangleFeature: rects1[0]))
29 |
30 | rects1 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect2, withCount: 10)
31 | rects2 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: 10)
32 |
33 | rectangles = rects1 + rects2
34 |
35 | biggestRectangle = rectangles.biggest()
36 | XCTAssert(biggestRectangle!.isWithin(1.0, ofRectangleFeature: rects2[0]))
37 |
38 | rects1 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: 10)
39 | rects2 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect2, withCount: 10)
40 | let rects3 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect3, withCount: 10)
41 |
42 | rectangles = rects1 + rects2 + rects3
43 |
44 | biggestRectangle = rectangles.biggest()
45 | XCTAssert(biggestRectangle!.isWithin(1.0, ofRectangleFeature: rects3[0]))
46 | }
47 |
48 | func testBiggestRectangleConsistentForSingleElement() {
49 | let singleRectangle: [Quadrilateral] = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: 1)
50 | XCTAssertNotNil(singleRectangle.biggest())
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CGAffineTransformTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransformTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class CGAffineTransformTests: XCTestCase {
13 |
14 | func testScalesUpCorrectly() {
15 |
16 | let fromSize = CGSize(width: 1, height: 1)
17 | let toSize = CGSize(width: 2, height: 2)
18 |
19 | let scale = CGAffineTransform.scaleTransform(forSize: fromSize, aspectFillInSize: toSize)
20 |
21 | XCTAssertEqual(scale.a, 2)
22 | XCTAssertEqual(scale.d, 2)
23 | }
24 |
25 | func testScalesDownCorrectly() {
26 |
27 | let fromSize = CGSize(width: 2, height: 2)
28 | let toSize = CGSize(width: 1, height: 1)
29 |
30 | let scale = CGAffineTransform.scaleTransform(forSize: fromSize, aspectFillInSize: toSize)
31 |
32 | XCTAssertEqual(scale.a, 0.5)
33 | XCTAssertEqual(scale.d, 0.5)
34 | }
35 |
36 | func testTranslatesCorrectly() {
37 |
38 | let fromRect = CGRect(x: 0, y: 0, width: 10, height: 10)
39 | let toRect = CGRect(x: 5, y: 5, width: 10, height: 10)
40 |
41 | let translate = CGAffineTransform.translateTransform(fromCenterOfRect: fromRect, toCenterOfRect: toRect)
42 |
43 | XCTAssertEqual(translate.a, 1.0)
44 | XCTAssertEqual(translate.d, 1.0)
45 | XCTAssertEqual(translate.tx, 5.0)
46 | XCTAssertEqual(translate.ty, 5.0)
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CGPointTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGPointTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Boris Emorine on 2/19/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class CGPointTests: XCTestCase {
13 |
14 | func testSurroundingRect() {
15 | var point = CGPoint.zero
16 | var rect = point.surroundingSquare(withSize: 10.0)
17 | var expectedRect = CGRect(x: -5.0, y: -5.0, width: 10.0, height: 10.0)
18 | XCTAssert(rect == expectedRect)
19 |
20 | point = CGPoint(x: 50.0, y: 50.0)
21 | rect = point.surroundingSquare(withSize: 20.0)
22 | expectedRect = CGRect(x: 40.0, y: 40.0, width: 20.0, height: 20.0)
23 | XCTAssert(rect == expectedRect)
24 | }
25 |
26 | func testIsWithinDelta() {
27 | let point1 = CGPoint.zero
28 | var point2 = CGPoint.zero
29 |
30 | XCTAssert(point1.isWithin(delta: 10.0, ofPoint: point2) == true)
31 | XCTAssert(point1.isWithin(delta: 0.0, ofPoint: point2) == true)
32 |
33 | point2 = CGPoint(x: 1.0, y: 1.0)
34 |
35 | XCTAssert(point1.isWithin(delta: 1.1, ofPoint: point2) == true)
36 | XCTAssert(point1.isWithin(delta: 0.9, ofPoint: point2) == false)
37 | }
38 |
39 | func testDistanceTo() {
40 | var point1 = CGPoint.zero
41 | var point2 = CGPoint(x: 10.0, y: 10.0)
42 |
43 | var distance = point1.distanceTo(point: point2)
44 | XCTAssertTrue(distance == 14.142135623730951)
45 |
46 | point1 = CGPoint(x: -1, y: 3)
47 | point2 = CGPoint(x: 3, y: -4)
48 | distance = point1.distanceTo(point: point2)
49 | XCTAssertTrue(distance == 8.06225774829855)
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CGRectTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRectTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Boris Emorine on 2/26/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class CGRectTests: XCTestCase {
13 |
14 | func testScaleAndCenter() {
15 | var rect = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
16 | rect = rect.scaleAndCenter(withRatio: 0.5)
17 | var expectedRect = CGRect(x: 25.0, y: 25.0, width: 50.0, height: 50.0)
18 |
19 | XCTAssert(rect == expectedRect)
20 |
21 | rect = CGRect(x: 100.0, y: 100.0, width: 200.0, height: 200.0)
22 | rect = rect.scaleAndCenter(withRatio: 0.75)
23 | expectedRect = CGRect(x: 125.0, y: 125.0, width: 150.0, height: 150.0)
24 |
25 | XCTAssert(rect == expectedRect)
26 |
27 | rect = CGRect(x: 100.0, y: 100.0, width: 200.0, height: 200.0)
28 | rect = rect.scaleAndCenter(withRatio: 2.0)
29 | expectedRect = CGRect(x: 0.0, y: 0.0, width: 400.0, height: 400.0)
30 |
31 | XCTAssert(rect == expectedRect)
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CGSizeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGSizeTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Julian Schiavo on 18/2/2019.
6 | // Copyright © 2019 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class CGSizeTests: XCTestCase {
13 |
14 | func testScaleFactorIsWithinMax() {
15 | let size = CGSize(width: 5000, height: 1000)
16 | let scaleFactor = size.scaleFactor(forMaxWidth: 1000, maxHeight: 5000)
17 |
18 | let updatedSize = CGSize(width: size.width * scaleFactor, height: size.height * scaleFactor)
19 |
20 | XCTAssert(updatedSize.width <= 1000)
21 | XCTAssert(updatedSize.width <= 5000)
22 | }
23 |
24 | func testScaleFactorCorrectWhenBelowMax() {
25 | let size = CGSize(width: 10, height: 10)
26 | let scaleFactor = size.scaleFactor(forMaxWidth: 1000, maxHeight: 5000)
27 |
28 | XCTAssertEqual(scaleFactor, 1)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CIRectangleDetectorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CIRectangleDetectorTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import SnapshotTesting
10 | @testable import WeScan
11 | import XCTest
12 |
13 | final class CIRectangleDetectorTests: XCTestCase {
14 |
15 | func testCorrectlyDetectsAndReturnsQuadilateral() {
16 |
17 | let targetSize = CGSize(width: 150, height: 150)
18 |
19 | let containerLayer = CALayer()
20 | containerLayer.backgroundColor = UIColor.white.cgColor
21 | containerLayer.frame = CGRect(origin: .zero, size: targetSize)
22 | containerLayer.masksToBounds = true
23 |
24 | let targetLayer = CALayer()
25 | targetLayer.backgroundColor = UIColor.black.cgColor
26 | targetLayer.frame = containerLayer.frame.insetBy(dx: 5, dy: 5)
27 |
28 | containerLayer.addSublayer(targetLayer)
29 |
30 | UIGraphicsBeginImageContextWithOptions(targetSize, true, 0.0)
31 |
32 | containerLayer.render(in: UIGraphicsGetCurrentContext()!)
33 |
34 | let image = UIGraphicsGetImageFromCurrentImageContext()!
35 | UIGraphicsEndImageContext()
36 |
37 | let ciImage = CIImage(cgImage: image.cgImage!)
38 | let quad = CIRectangleDetector.rectangle(forImage: ciImage)!
39 |
40 | let resultView = UIView(frame: containerLayer.frame)
41 | resultView.layer.addSublayer(containerLayer)
42 |
43 | let quadView = QuadrilateralView(frame: resultView.bounds)
44 | quadView.drawQuadrilateral(quad: quad, animated: false)
45 | quadView.backgroundColor = UIColor.red
46 | resultView.addSubview(quadView)
47 |
48 | assertSnapshot(matching: resultView, as: .image(perceptualPrecision: 0.97))
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/CaptureSessionTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureSessionTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | @testable import WeScan
11 | import XCTest
12 |
13 | final class CaptureSessionTests: XCTestCase {
14 |
15 | private let session = CaptureSession.current
16 |
17 | func testAutoScanEnabledByDefault() {
18 | XCTAssertTrue(session.isAutoScanEnabled)
19 | }
20 |
21 | func testEditOrientationUpByDefault() {
22 | XCTAssertEqual(session.editImageOrientation, CGImagePropertyOrientation.up)
23 | }
24 |
25 | func testCaptureDeviceIsAvailable() {
26 | session.device = MockCaptureDevice()
27 | XCTAssertNotNil(session.device)
28 | }
29 |
30 | func testCanToggleFlash() {
31 | session.device = MockCaptureDevice()
32 |
33 | let state = session.toggleFlash()
34 | XCTAssertEqual(state, CaptureSession.FlashState.on)
35 | }
36 |
37 | func testCanSetFocusPoint() {
38 | session.device = MockCaptureDevice()
39 | XCTAssertNoThrow(try session.setFocusPointToTapPoint(.zero))
40 | }
41 |
42 | func testCanResetFocusToAuto() {
43 | session.device = MockCaptureDevice()
44 | XCTAssertNoThrow(try session.resetFocusToAuto())
45 | XCTAssertEqual(session.device?.focusMode, AVCaptureDevice.FocusMode.continuousAutoFocus)
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/FocusRectangleViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FocusRectangleViewTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Julian Schiavo on 24/11/2018.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class FocusRectangleViewTests: XCTestCase {
13 |
14 | func testFocusRectangleIsRemovedCorrectly() {
15 | let hostView = UIView()
16 | let session = CaptureSession.current
17 |
18 | let focusRectangle = FocusRectangleView(touchPoint: CGPoint(x: 1, y: 1))
19 | hostView.addSubview(focusRectangle)
20 | session.removeFocusRectangleIfNeeded(focusRectangle, animated: false)
21 |
22 | XCTAssertTrue(focusRectangle.superview == nil)
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/ImageFeatureTestHelpers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageFeatureTestHelpers.swift
3 | // WeScanTests
4 | //
5 | // Created by Boris Emorine on 2/24/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | import UIKit
11 | @testable import WeScan
12 |
13 | enum ResourceImage: String {
14 | case rect1 = "Square.jpg"
15 | case rect2 = "Rectangle.jpg"
16 | case rect3 = "BigRectangle.jpg"
17 | }
18 |
19 | final class ImageFeatureTestHelpers: NSObject {
20 |
21 | static func getRectangleFeatures(from resourceImage: ResourceImage, withCount count: Int) -> [Quadrilateral] {
22 | var rectangleFeatures = [Quadrilateral]()
23 |
24 | for _ in 0 ..< count {
25 | rectangleFeatures.append(ImageFeatureTestHelpers.getRectangleFeature(from: resourceImage))
26 | }
27 |
28 | return rectangleFeatures
29 | }
30 |
31 | static func getRectangleFeature(from resourceImage: ResourceImage) -> Quadrilateral {
32 | let image = UIImage(named: resourceImage.rawValue, in: Bundle.module, compatibleWith: nil)
33 | let ciImage = CIImage(image: image!)!
34 |
35 | return CIRectangleDetector.rectangle(forImage: ciImage)!
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/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 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/QuadrilateralViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QuadrilateralViewTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Bobo on 6/9/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 | // swiftlint:disable line_length
9 |
10 | @testable import WeScan
11 | import XCTest
12 |
13 | final class QuadrilateralViewTests: XCTestCase {
14 |
15 | let vc = UIViewController()
16 |
17 | override func setUp() {
18 | super.setUp()
19 | vc.loadView()
20 | }
21 |
22 | func testNonEditable() {
23 | let quadView = QuadrilateralView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
24 | quadView.editable = false
25 | vc.view.addSubview(quadView)
26 |
27 | let topLeftCornerView = quadView.cornerViewForCornerPosition(position: .topLeft)
28 |
29 | XCTAssertTrue(topLeftCornerView.isHidden, "A non editable QuadrilateralView should have hidden corners")
30 | }
31 |
32 | func testHightlight() {
33 | let quadView = QuadrilateralView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
34 | quadView.editable = true
35 | vc.view.addSubview(quadView)
36 |
37 | let quad = Quadrilateral(topLeft: .zero, topRight: CGPoint(x: 200.0, y: 0.0), bottomRight: CGPoint(x: 200.0, y: 200.0), bottomLeft: CGPoint(x: 0.0, y: 220.0))
38 |
39 | quadView.drawQuadrilateral(quad: quad, animated: false)
40 |
41 | let topRightCornerView = quadView.cornerViewForCornerPosition(position: .topRight)
42 |
43 | XCTAssertFalse(topRightCornerView.isHidden, "An editable QuadrilateralView should not have hidden corners")
44 |
45 | let defaultTopLeftCornerViewFrame = topRightCornerView.frame
46 |
47 | quadView.highlightCornerAtPosition(position: .topRight, with: UIImage())
48 |
49 | let highlitedTopLeftCornerViewFrame = topRightCornerView.frame
50 |
51 | XCTAssert(defaultTopLeftCornerViewFrame.width < highlitedTopLeftCornerViewFrame.width, "A highlighted corner view should be bigger than in its default state")
52 | XCTAssert(defaultTopLeftCornerViewFrame.height < highlitedTopLeftCornerViewFrame.height, "A highlighted corner view should be bigger than in its default state")
53 | XCTAssertEqual(defaultTopLeftCornerViewFrame.origin.x + defaultTopLeftCornerViewFrame.width / 2.0, highlitedTopLeftCornerViewFrame.origin.x + highlitedTopLeftCornerViewFrame.width / 2.0, "A highlighted corner view should have the same center as in its default state")
54 | XCTAssertEqual(defaultTopLeftCornerViewFrame.origin.y + defaultTopLeftCornerViewFrame.height / 2.0, highlitedTopLeftCornerViewFrame.origin.y + highlitedTopLeftCornerViewFrame.height / 2.0, "A highlighted corner view should have the same center as in its default state")
55 | XCTAssertTrue(topRightCornerView.isHighlighted)
56 |
57 | quadView.resetHighlightedCornerViews()
58 |
59 | XCTAssert(defaultTopLeftCornerViewFrame.width == topRightCornerView.frame.width, "After reseting, the corner view frame should be back to its inital value")
60 | XCTAssert(defaultTopLeftCornerViewFrame.height == topRightCornerView.frame.height, "After reseting, the corner view frame should be back to its inital value")
61 | XCTAssertEqual(defaultTopLeftCornerViewFrame.origin.x + defaultTopLeftCornerViewFrame.width / 2.0, topRightCornerView.center.x, "After reseting, the corner view center should still not have moved")
62 | XCTAssertEqual(defaultTopLeftCornerViewFrame.origin.y + defaultTopLeftCornerViewFrame.height / 2.0, topRightCornerView.center.y, "After reseting, the corner view center should still not have moved")
63 | XCTAssertFalse(topRightCornerView.isHighlighted)
64 | }
65 |
66 | func testDrawQuad() {
67 | let quadView = QuadrilateralView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 200.0))
68 | quadView.editable = true
69 | vc.view.addSubview(quadView)
70 |
71 | let quad = Quadrilateral(topLeft: .zero, topRight: CGPoint(x: 200.0, y: 0.0), bottomRight: CGPoint(x: 200.0, y: 200.0), bottomLeft: CGPoint(x: 0.0, y: 220.0))
72 |
73 | quadView.drawQuadrilateral(quad: quad, animated: false)
74 |
75 | let topLeftCornerView = quadView.cornerViewForCornerPosition(position: .topLeft)
76 | XCTAssertEqual(topLeftCornerView.center, quad.topLeft)
77 |
78 | let topRightCornerView = quadView.cornerViewForCornerPosition(position: .topRight)
79 | XCTAssertEqual(topRightCornerView.center, quad.topRight)
80 |
81 | let bottomRightCornerView = quadView.cornerViewForCornerPosition(position: .bottomRight)
82 | XCTAssertEqual(bottomRightCornerView.center, quad.bottomRight)
83 |
84 | let bottomLeftCornerView = quadView.cornerViewForCornerPosition(position: .bottomLeft)
85 | XCTAssertEqual(bottomLeftCornerView.center, quad.bottomLeft)
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/RectangleFeaturesFunnelTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RectangleFeaturesFunnelTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Boris Emorine on 2/24/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | @testable import WeScan
10 | import XCTest
11 |
12 | final class RectangleFeaturesFunnelTests: XCTestCase {
13 |
14 | var funnel = RectangleFeaturesFunnel()
15 |
16 | override func setUp() {
17 | super.setUp()
18 | funnel = RectangleFeaturesFunnel()
19 | }
20 |
21 | /// Ensures that feeding the funnel with less than the minimum number of rectangles doesn't trigger the completion block.
22 | func testAddMinUnderThreshold() {
23 | let rectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: funnel.minNumberOfRectangles - 1)
24 |
25 | let expectation = XCTestExpectation(description: "Funnel add callback")
26 | expectation.isInverted = true
27 |
28 | for i in 0 ..< rectangleFeatures.count {
29 | funnel.add(rectangleFeatures[i], currentlyDisplayedRectangle: nil) { _, _ in
30 | expectation.fulfill()
31 | }
32 | }
33 |
34 | wait(for: [expectation], timeout: 3.0)
35 | }
36 |
37 | /// Ensures that feeding the funnel with the minimum number of rectangles triggers the completion block.
38 | func testAddMinThreshold() {
39 | let rectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: funnel.minNumberOfRectangles)
40 |
41 | let expectation = XCTestExpectation(description: "Funnel add callback")
42 |
43 | for i in 0 ..< rectangleFeatures.count {
44 | funnel.add(rectangleFeatures[i], currentlyDisplayedRectangle: nil) { _, _ in
45 | expectation.fulfill()
46 | }
47 | }
48 |
49 | wait(for: [expectation], timeout: 3.0)
50 | }
51 |
52 | /// Ensures that feeding the funnel with a lot of rectangles triggers the completion block the appropriate amount of time.
53 | func testAddMaxThreshold() {
54 | let rectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: funnel.maxNumberOfRectangles * 2)
55 |
56 | let expectation = XCTestExpectation(description: "Funnel add callback")
57 | expectation.expectedFulfillmentCount = rectangleFeatures.count - funnel.minNumberOfRectangles
58 |
59 | for i in 0 ..< rectangleFeatures.count {
60 | funnel.add(rectangleFeatures[i], currentlyDisplayedRectangle: nil) { _, _ in
61 | expectation.fulfill()
62 | }
63 | }
64 |
65 | wait(for: [expectation], timeout: 3.0)
66 | }
67 |
68 | /// Ensures that feeding the funnel with rectangles similar to the currently displayed one doesn't trigger the completion block.
69 | func testAddPreviouslyDisplayedRect() {
70 | let rectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: funnel.maxNumberOfRectangles * 2)
71 |
72 | let expectation = XCTestExpectation(description: "Funnel add callback")
73 | expectation.isInverted = true
74 |
75 | let currentlyDisplayedRect = rectangleFeatures.first!
76 |
77 | for i in 0 ..< rectangleFeatures.count {
78 | funnel.add(rectangleFeatures[i], currentlyDisplayedRectangle: currentlyDisplayedRect) { _, _ in
79 | expectation.fulfill()
80 | }
81 | }
82 |
83 | wait(for: [expectation], timeout: 3.0)
84 | }
85 |
86 | /// Ensures that feeding the funnel with 2 images alternatively (image 1, image 2, image 1, image 2, image 1 etc.),
87 | /// doesn't make the completion block get called every time a new one is added.
88 | func testAddAlternateImage() {
89 | let count = 100
90 | let type1RectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: count)
91 | let type2RectangleFeatures = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect2, withCount: count)
92 | var currentlyDisplayedRect: Quadrilateral?
93 |
94 | let expectation = XCTestExpectation(description: "Funnel add callback")
95 | expectation.isInverted = true
96 |
97 | for i in 0 ..< count {
98 | let rectangleFeature = i % 2 == 0 ? type1RectangleFeatures[i] : type2RectangleFeatures[i]
99 |
100 | funnel.add(rectangleFeature, currentlyDisplayedRectangle: currentlyDisplayedRect, completion: { result, rectFeature in
101 |
102 | currentlyDisplayedRect = rectFeature
103 | if i >= funnel.maxNumberOfRectangles && result == .showOnly {
104 | expectation.fulfill()
105 | }
106 | })
107 | }
108 |
109 | wait(for: [expectation], timeout: 3.0)
110 | }
111 |
112 | func testAddMultipleImages() {
113 | let count = max(funnel.minNumberOfMatches + 2, funnel.minNumberOfRectangles)
114 |
115 | let rectangleFeaturesType1 = ImageFeatureTestHelpers.getRectangleFeatures(from: .rect1, withCount: count - 1)
116 | let rectangleFeatureType2 = ImageFeatureTestHelpers.getRectangleFeature(from: .rect2)
117 |
118 | for i in 0 ..< rectangleFeaturesType1.count {
119 | funnel.add(rectangleFeaturesType1[i], currentlyDisplayedRectangle: nil, completion: {_, _ in
120 | })
121 | }
122 |
123 | let expectationType1 = XCTestExpectation(description: "Funnel add callback")
124 |
125 | funnel.add(rectangleFeatureType2, currentlyDisplayedRectangle: nil) { _, rectangle in
126 | XCTAssert(rectangle.isWithin(1.0, ofRectangleFeature: rectangleFeaturesType1[0]))
127 | expectationType1.fulfill()
128 | }
129 |
130 | wait(for: [expectationType1], timeout: 3.0)
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/Resources/BigRectangle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/Resources/BigRectangle.jpg
--------------------------------------------------------------------------------
/Tests/WeScanTests/Resources/Rectangle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/Resources/Rectangle.jpg
--------------------------------------------------------------------------------
/Tests/WeScanTests/Resources/Square.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/Resources/Square.jpg
--------------------------------------------------------------------------------
/Tests/WeScanTests/ReviewViewControllerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReviewViewControllerTests.swift
3 | // WeScanTests
4 | //
5 | // Created by Julian Schiavo on 7/1/2019.
6 | // Copyright © 2019 WeTransfer. All rights reserved.
7 | //
8 | // swiftlint:disable line_length
9 |
10 | import CoreGraphics
11 | import SnapshotTesting
12 | @testable import WeScan
13 | import XCTest
14 |
15 | final class ReviewViewControllerTests: XCTestCase {
16 |
17 | var demoScan: ImageScannerScan!
18 | var enhancedDemoScan: ImageScannerScan!
19 | var demoQuad = Quadrilateral(topLeft: .zero, topRight: .zero, bottomRight: .zero, bottomLeft: .zero)
20 |
21 | override func setUp() {
22 | super.setUp()
23 |
24 | // Set up the demo image using rectangles (purposefully made to be different on each rotation)
25 | let detailSize = CGSize(width: 20, height: 40)
26 | let detailLayer = CALayer()
27 | detailLayer.backgroundColor = UIColor.red.cgColor
28 | detailLayer.frame = CGRect(x: 30, y: 0, width: detailSize.width, height: detailSize.height)
29 |
30 | let backgroundSize = CGSize(width: 50, height: 120)
31 | let backgroundLayer = CALayer()
32 | backgroundLayer.backgroundColor = UIColor.white.cgColor
33 | backgroundLayer.frame = CGRect(x: 0, y: 0, width: backgroundSize.width, height: backgroundSize.height)
34 | backgroundLayer.addSublayer(detailLayer)
35 |
36 | UIGraphicsBeginImageContextWithOptions(backgroundSize, true, 1.0)
37 | backgroundLayer.render(in: UIGraphicsGetCurrentContext()!)
38 | let image = UIGraphicsGetImageFromCurrentImageContext()!
39 | demoScan = ImageScannerScan(image: image)
40 |
41 | backgroundLayer.backgroundColor = UIColor.black.cgColor
42 | backgroundLayer.render(in: UIGraphicsGetCurrentContext()!)
43 | let enhancedImage = UIGraphicsGetImageFromCurrentImageContext()!
44 | enhancedDemoScan = ImageScannerScan(image: enhancedImage)
45 |
46 | UIGraphicsEndImageContext()
47 | }
48 |
49 | func testDemoImageIsCorrect() {
50 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: demoScan, doesUserPreferEnhancedScan: false)
51 | let vc = ReviewViewController(results: results)
52 | vc.viewDidLoad()
53 | assertSnapshot(matching: vc.imageView, as: .image)
54 | }
55 |
56 | func testImageIsCorrectlyRotated90() {
57 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: demoScan, doesUserPreferEnhancedScan: false)
58 | let vc = ReviewViewController(results: results)
59 | vc.viewDidLoad()
60 | vc.rotateImage()
61 | assertSnapshot(matching: vc.imageView, as: .image)
62 | }
63 |
64 | func testImageIsCorrectlyRotated180() {
65 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: demoScan, doesUserPreferEnhancedScan: false)
66 | let vc = ReviewViewController(results: results)
67 | vc.viewDidLoad()
68 |
69 | vc.rotateImage()
70 | vc.rotateImage()
71 |
72 | assertSnapshot(matching: vc.imageView, as: .image)
73 | }
74 |
75 | func testImageIsCorrectlyRotated270() {
76 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: demoScan, doesUserPreferEnhancedScan: false)
77 | let vc = ReviewViewController(results: results)
78 | vc.viewDidLoad()
79 |
80 | vc.rotateImage()
81 | vc.rotateImage()
82 | vc.rotateImage()
83 |
84 | assertSnapshot(matching: vc.imageView, as: .image)
85 | }
86 |
87 | func testImageIsCorrectlyRotated360() {
88 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: demoScan, doesUserPreferEnhancedScan: false)
89 | let vc = ReviewViewController(results: results)
90 | vc.viewDidLoad()
91 |
92 | vc.rotateImage()
93 | vc.rotateImage()
94 | vc.rotateImage()
95 | vc.rotateImage()
96 |
97 | assertSnapshot(matching: vc.imageView, as: .image)
98 | }
99 |
100 | func testEnhancedImage() {
101 | let results = ImageScannerResults(detectedRectangle: demoQuad, originalScan: demoScan, croppedScan: demoScan, enhancedScan: enhancedDemoScan, doesUserPreferEnhancedScan: false)
102 | let viewController = ReviewViewController(results: results)
103 | viewController.viewDidLoad()
104 |
105 | viewController.toggleEnhancedImage()
106 | assertSnapshot(matching: viewController.imageView, as: .image)
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/UIImageTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import SnapshotTesting
10 | @testable import WeScan
11 | import XCTest
12 |
13 | final class UIImageTests: XCTestCase {
14 |
15 | func testRotateUpFacingImageCorrectly() {
16 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
17 | let orientatedImage = UIImage(cgImage: image!.cgImage!, scale: 1.0, orientation: .up)
18 |
19 | let view = UIImageView(image: orientatedImage.applyingPortraitOrientation())
20 | view.sizeToFit()
21 |
22 | assertSnapshot(matching: view, as: .image)
23 | }
24 |
25 | func testRotateDownFacingImageCorrectly() {
26 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
27 | let orientatedImage = UIImage(cgImage: image!.cgImage!, scale: 1.0, orientation: .down)
28 |
29 | let view = UIImageView(image: orientatedImage.applyingPortraitOrientation())
30 | view.sizeToFit()
31 |
32 | assertSnapshot(matching: view, as: .image)
33 | }
34 |
35 | func testRotateLeftFacingImageCorrectly() {
36 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
37 | let orientatedImage = UIImage(cgImage: image!.cgImage!, scale: 1.0, orientation: .left)
38 |
39 | let view = UIImageView(image: orientatedImage.applyingPortraitOrientation())
40 | view.sizeToFit()
41 |
42 | assertSnapshot(matching: view, as: .image)
43 | }
44 |
45 | func testRotateRightFacingImageCorrectly() {
46 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
47 | let orientatedImage = UIImage(cgImage: image!.cgImage!, scale: 1.0, orientation: .right)
48 |
49 | let view = UIImageView(image: orientatedImage.applyingPortraitOrientation())
50 | view.sizeToFit()
51 |
52 | assertSnapshot(matching: view, as: .image)
53 | }
54 |
55 | func testRotateDefaultFacingImageCorrectly() {
56 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
57 | let orientatedImage = UIImage(cgImage: image!.cgImage!, scale: 1.0, orientation: .rightMirrored)
58 |
59 | let view = UIImageView(image: orientatedImage.applyingPortraitOrientation())
60 | view.sizeToFit()
61 |
62 | assertSnapshot(matching: view, as: .image)
63 | }
64 |
65 | func testRotateImageCorrectly() {
66 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)
67 |
68 | let view = UIImageView(image: image!.rotated(by: Measurement(value: Double.pi * 0.2, unit: .radians), options: []))
69 | view.sizeToFit()
70 |
71 | assertSnapshot(matching: view, as: .image)
72 | }
73 |
74 | func testScaledImageSuccessfully() {
75 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)!
76 | XCTAssertNotNil(image.scaledImage(scaleFactor: 0.2))
77 | }
78 |
79 | func testScaledImageCorrectly() {
80 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)!
81 | XCTAssertEqual(image.size, CGSize(width: 500, height: 500))
82 | XCTAssertEqual(image.scaledImage(scaleFactor: 0.2)!.size, CGSize(width: 100, height: 100))
83 | }
84 |
85 | func testPDFDataCreationSuccessful() {
86 | let image = UIImage(named: ResourceImage.rect2.rawValue, in: Bundle.module, compatibleWith: nil)!
87 | XCTAssertNotNil(image.pdfData())
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/VisionRectangleDetectorTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VisionRectangleDetectorTests.swift
3 | // WeScanTests
4 | //
5 | // Created by James Campbell on 8/8/18.
6 | // Copyright © 2018 WeTransfer. All rights reserved.
7 | //
8 |
9 | import SnapshotTesting
10 | @testable import WeScan
11 | import XCTest
12 |
13 | final class VisionRectangleDetectorTests: XCTestCase {
14 |
15 | private var containerLayer: CALayer!
16 | private var image: UIImage!
17 |
18 | override func setUp() {
19 | super.setUp()
20 |
21 | // Setting up containerLayer and creating the image to be tested on both tests in this class.
22 | containerLayer = CALayer()
23 | let targetSize = CGSize(width: 150, height: 150)
24 |
25 | containerLayer.backgroundColor = UIColor.white.cgColor
26 | containerLayer.frame = CGRect(origin: .zero, size: targetSize)
27 | containerLayer.masksToBounds = true
28 |
29 | let targetLayer = CALayer()
30 | targetLayer.backgroundColor = UIColor.black.cgColor
31 | targetLayer.frame = containerLayer.frame.insetBy(dx: 5, dy: 5)
32 |
33 | containerLayer.addSublayer(targetLayer)
34 |
35 | UIGraphicsBeginImageContextWithOptions(targetSize, true, 0.0)
36 |
37 | containerLayer.render(in: UIGraphicsGetCurrentContext()!)
38 |
39 | self.image = UIGraphicsGetImageFromCurrentImageContext()!
40 |
41 | UIGraphicsEndImageContext()
42 |
43 | }
44 |
45 | override func tearDown() {
46 | super.tearDown()
47 | containerLayer = nil
48 | image = nil
49 | }
50 |
51 | func testCorrectlyDetectsAndReturnsQuadilateral() {
52 |
53 | let ciImage = CIImage(cgImage: image.cgImage!)
54 | let expectation = XCTestExpectation(description: "Detect rectangle on CIImage")
55 |
56 | VisionRectangleDetector.rectangle(forImage: ciImage) { quad in
57 |
58 | DispatchQueue.main.async {
59 |
60 | let resultView = UIView(frame: self.containerLayer.frame)
61 | resultView.layer.addSublayer(self.containerLayer)
62 |
63 | let quadView = QuadrilateralView(frame: resultView.bounds)
64 | quadView.drawQuadrilateral(quad: quad!, animated: false)
65 | quadView.backgroundColor = UIColor.red
66 | resultView.addSubview(quadView)
67 |
68 | assertSnapshot(matching: resultView, as: .image)
69 | expectation.fulfill()
70 | }
71 | }
72 |
73 | wait(for: [expectation], timeout: 3.0)
74 | }
75 |
76 | func testCorrectlyDetectsAndReturnsQuadilateralPixelBuffer() {
77 |
78 | let expectation = XCTestExpectation(description: "Detect rectangle on CVPixelBuffer")
79 | if let pixelBuffer = image.pixelBuffer() {
80 | VisionRectangleDetector.rectangle(forPixelBuffer: pixelBuffer) { quad in
81 |
82 | DispatchQueue.main.async {
83 |
84 | let resultView = UIView(frame: self.containerLayer.frame)
85 | resultView.layer.addSublayer(self.containerLayer)
86 |
87 | let quadView = QuadrilateralView(frame: resultView.bounds)
88 | quadView.drawQuadrilateral(quad: quad!, animated: false)
89 | quadView.backgroundColor = UIColor.red
90 | resultView.addSubview(quadView)
91 |
92 | assertSnapshot(matching: resultView, as: .image)
93 | expectation.fulfill()
94 | }
95 | }
96 | } else {
97 | XCTFail("could not convert image to pixelBuffer")
98 | }
99 |
100 | wait(for: [expectation], timeout: 3.0)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/WeScanTests-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 |
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/CIRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateral.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/CIRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateral.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testDemoImageIsCorrect.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testDemoImageIsCorrect.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testEnhancedImage.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testEnhancedImage.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated180.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated180.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated270.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated270.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated360.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated360.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated90.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/ReviewViewControllerTests/testImageIsCorrectlyRotated90.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateDefaultFacingImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateDefaultFacingImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateDownFacingImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateDownFacingImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateLeftFacingImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateLeftFacingImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateRightFacingImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateRightFacingImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateUpFacingImageCorrectly.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/UIImageTests/testRotateUpFacingImageCorrectly.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/VisionRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateral.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/VisionRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateral.1.png
--------------------------------------------------------------------------------
/Tests/WeScanTests/__Snapshots__/VisionRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateralPixelBuffer.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Tests/WeScanTests/__Snapshots__/VisionRectangleDetectorTests/testCorrectlyDetectsAndReturnsQuadilateralPixelBuffer.1.png
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # Fastlane requirements
2 | fastlane_version "1.109.0"
3 |
4 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/testing_lanes.rb"
5 | import "./../Submodules/WeTransfer-iOS-CI/Fastlane/shared_lanes.rb"
6 |
7 | desc "Run the tests and prepare for Danger"
8 | lane :test do |options|
9 | test_package(
10 | package_name: 'WeScan',
11 | package_path: ENV['PWD'],
12 | disable_automatic_package_resolution: false,
13 | device: "iPhone 14 Pro"
14 | )
15 | end
16 |
--------------------------------------------------------------------------------