├── .github ├── CODEOWNERS └── workflows │ └── stale.yml ├── .gitignore ├── .gitmodules ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ ├── xcshareddata │ └── xcschemes │ │ └── WeScan.xcscheme │ └── xcuserdata │ └── avanderlee.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Assets ├── LinkedFrameworks.png ├── WeScan-Banner.jpg ├── WeScan.gif └── project.png ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Changelog.md ├── Example ├── WeScan.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── WeScanSampleProject │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── WeScanAppIcon20pt@2x.jpg │ │ ├── WeScanAppIcon20pt@3x.jpg │ │ ├── WeScanAppIcon29pt@2x.jpg │ │ ├── WeScanAppIcon29pt@3x.jpg │ │ ├── WeScanAppIcon40pt@2x.jpg │ │ ├── WeScanAppIcon40pt@3x.jpg │ │ ├── WeScanAppIcon60pt@2x.jpg │ │ └── WeScanAppIcon60pt@3x.jpg │ ├── Contents.json │ └── WeScanLogo.imageset │ │ ├── Contents.json │ │ ├── WeScanLogo.png │ │ ├── WeScanLogo@2x.png │ │ └── WeScanLogo@3x.png │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── EditImageViewController.swift │ ├── HomeViewController.swift │ ├── Info.plist │ ├── NewCameraViewController.swift │ ├── ReviewImageViewController.swift │ └── nl.lproj │ ├── LaunchScreen.strings │ └── Main.strings ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── WeScan │ ├── Common │ ├── CIRectangleDetector.swift │ ├── EditScanCornerView.swift │ ├── Error.swift │ ├── Quadrilateral.swift │ ├── QuadrilateralView.swift │ └── VisionRectangleDetector.swift │ ├── Edit │ ├── EditImageViewController.swift │ ├── EditScanViewController.swift │ └── ZoomGestureController.swift │ ├── Extensions │ ├── AVCaptureVideoOrientation+Utils.swift │ ├── Array+Utils.swift │ ├── CGAffineTransform+Utils.swift │ ├── CGImagePropertyOrientation.swift │ ├── CGPoint+Utils.swift │ ├── CGRect+Utils.swift │ ├── CGSize+Utils.swift │ ├── CIImage+Utils.swift │ ├── UIImage+Orientation.swift │ ├── UIImage+SFSymbol.swift │ └── UIImage+Utils.swift │ ├── ImageScannerController.swift │ ├── Protocols │ ├── CaptureDevice.swift │ └── Transformable.swift │ ├── Resources │ ├── Assets │ │ ├── enhance.png │ │ ├── enhance@2x.png │ │ ├── enhance@3x.png │ │ ├── flash.png │ │ ├── flash@2x.png │ │ ├── flash@3x.png │ │ ├── flashUnavailable.png │ │ ├── flashUnavailable@2x.png │ │ ├── flashUnavailable@3x.png │ │ ├── rotate.png │ │ ├── rotate@2x.png │ │ └── rotate@3x.png │ └── Localisation │ │ ├── ar.lproj │ │ └── Localizable.strings │ │ ├── cs.lproj │ │ └── Localizable.strings │ │ ├── de.lproj │ │ └── Localizable.strings │ │ ├── en.lproj │ │ └── Localizable.strings │ │ ├── es-419.lproj │ │ └── Localizable.strings │ │ ├── es.lproj │ │ └── Localizable.strings │ │ ├── fr.lproj │ │ └── Localizable.strings │ │ ├── hu.lproj │ │ └── Localizable.strings │ │ ├── it.lproj │ │ └── Localizable.strings │ │ ├── ko.lproj │ │ └── Localizable.strings │ │ ├── nl.lproj │ │ └── Localizable.strings │ │ ├── pl.lproj │ │ └── Localizable.strings │ │ ├── pt-BR.lproj │ │ └── Localizable.strings │ │ ├── pt-PT.lproj │ │ └── Localizable.strings │ │ ├── ru.lproj │ │ └── Localizable.strings │ │ ├── sv.lproj │ │ └── Localizable.strings │ │ ├── tr.lproj │ │ └── Localizable.strings │ │ ├── zh-Hans.lproj │ │ └── Localizable.strings │ │ └── zh-Hant.lproj │ │ └── Localizable.strings │ ├── Review │ └── ReviewViewController.swift │ ├── Scan │ ├── CameraScannerViewController.swift │ ├── CaptureSessionManager.swift │ ├── FocusRectangleView.swift │ ├── RectangleFeaturesFunnel.swift │ ├── ScannerViewController.swift │ └── ShutterButton.swift │ ├── Session │ ├── CaptureSession+Flash.swift │ ├── CaptureSession+Focus.swift │ ├── CaptureSession+Orientation.swift │ └── CaptureSession.swift │ └── ja.lproj │ └── Localizable.strings ├── Tests └── WeScanTests │ ├── AVCaptureVideoOrientationTests.swift │ ├── ArrayTests.swift │ ├── CGAffineTransformTests.swift │ ├── CGPointTests.swift │ ├── CGRectTests.swift │ ├── CGSizeTests.swift │ ├── CIRectangleDetectorTests.swift │ ├── CaptureSessionTests.swift │ ├── FocusRectangleViewTests.swift │ ├── ImageFeatureTestHelpers.swift │ ├── Info.plist │ ├── QuadrilateralTests.swift │ ├── QuadrilateralViewTests.swift │ ├── RectangleFeaturesFunnelTests.swift │ ├── Resources │ ├── BigRectangle.jpg │ ├── Rectangle.jpg │ └── Square.jpg │ ├── ReviewViewControllerTests.swift │ ├── UIImageTests.swift │ ├── VisionRectangleDetectorTests.swift │ ├── WeScanTests-Bridging-Header.h │ └── __Snapshots__ │ ├── CIRectangleDetectorTests │ └── testCorrectlyDetectsAndReturnsQuadilateral.1.png │ ├── ReviewViewControllerTests │ ├── testDemoImageIsCorrect.1.png │ ├── testEnhancedImage.1.png │ ├── testImageIsCorrectlyRotated180.1.png │ ├── testImageIsCorrectlyRotated270.1.png │ ├── testImageIsCorrectlyRotated360.1.png │ └── testImageIsCorrectlyRotated90.1.png │ ├── UIImageTests │ ├── testRotateDefaultFacingImageCorrectly.1.png │ ├── testRotateDownFacingImageCorrectly.1.png │ ├── testRotateImageCorrectly.1.png │ ├── testRotateLeftFacingImageCorrectly.1.png │ ├── testRotateRightFacingImageCorrectly.1.png │ └── testRotateUpFacingImageCorrectly.1.png │ └── VisionRectangleDetectorTests │ ├── testCorrectlyDetectsAndReturnsQuadilateral.1.png │ └── testCorrectlyDetectsAndReturnsQuadilateralPixelBuffer.1.png └── fastlane └── Fastfile /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/about-code-owners 2 | # These owners will be the default owners for everything in 3 | # the repo. Unless a later match takes precedence, they 4 | # will be requested for review when someone opens a PR. 5 | * @wetransfer/ios-collect 6 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v3 12 | with: 13 | repo-token: ${{ secrets.GITHUB_TOKEN }} 14 | stale-issue-label: 'Stale' 15 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove the Stale label or comment or this will be closed in 10 days.' 16 | stale-pr-label: 'Stale' 17 | stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove the Stale label or comment or this will be closed in 10 days.' 18 | exempt-issue-labels: 'bug,enhancement' 19 | days-before-stale: 30 20 | days-before-close: 10 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | ## Bundles 35 | Gemfile.lock 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | .build/ 42 | .spm-build 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/test_output 54 | fastlane/Readme.md 55 | screenshots/ 56 | screenshots/.DS_Store 57 | !*/screenshots/ 58 | Preview.html 59 | .DS_Store 60 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Submodules/WeTransfer-iOS-CI"] 2 | path = Submodules/WeTransfer-iOS-CI 3 | url = https://github.com/WeTransfer/WeTransfer-iOS-CI.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/WeScan.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 57 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | 75 | 81 | 82 | 83 | 84 | 85 | 95 | 96 | 102 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/avanderlee.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | WeScan.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Assets/LinkedFrameworks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Assets/LinkedFrameworks.png -------------------------------------------------------------------------------- /Assets/WeScan-Banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Assets/WeScan-Banner.jpg -------------------------------------------------------------------------------- /Assets/WeScan.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Assets/WeScan.gif -------------------------------------------------------------------------------- /Assets/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WeTransfer/WeScan/861003ae3acda7a515d0cbe95ea3a534e061ecd9/Assets/project.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at mobile@wetransfer.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WeScan 2 | 3 | As the creators, and maintainers of this project, we're glad to share our projects and invite contributors to help us stay up to date. Please take a moment to review this document in order to make the contribution process easy and effective for everyone involved. 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue or assessing patches and features. 6 | 7 | In general, we expect you to follow our [Code of Conduct](https://github.com/WeTransfer/WeScan/blob/master/CODE_OF_CONDUCT.md). 8 | 9 | ## Using Github Issues 10 | 11 | ### First time contributors 12 | We should encourage first time contributors. A good inspiration on this can be found [here](http://www.firsttimersonly.com/). As pointed out: 13 | 14 | > If you are an OSS project owner, then consider marking a few open issues with the label first-timers-only. The first-timers-only label explicitly announces: 15 | 16 | > "I'm willing to hold your hand so you can make your first PR. This issue is rather a bit easier than normal. And anyone who’s already contributed to open source isn’t allowed to touch this one!" 17 | 18 | By labeling issues with this `first-timers-only` label we help first time contributors step up their game and start contributing. 19 | 20 | ### Bug reports 21 | 22 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 23 | Good bug reports are extremely helpful - thank you! 24 | 25 | Guidelines for bug reports: 26 | 27 | 1. **Use the GitHub issue search** — check if the issue has already been 28 | reported. 29 | 30 | 2. **Check if the issue has been fixed** — try to reproduce it using the 31 | latest `master` or development branch in the repository. 32 | 33 | 3. **Isolate the problem** — provide clear steps to reproduce. 34 | 35 | A good bug report shouldn't leave others needing to chase you up for more information. 36 | 37 | Please try to be as detailed as possible in your report. What is your environment? What steps will reproduce the issue? What would you expect to be the outcome? All these details will help people to fix any potential bugs. 38 | 39 | Example: 40 | 41 | > Short and descriptive example bug report title 42 | > 43 | > A summary of the issue and the OS environment in which it occurs. If 44 | > suitable, include the steps required to reproduce the bug. 45 | > 46 | > 1. This is the first step 47 | > 2. This is the second step 48 | > 3. Further steps, etc. 49 | > 50 | > `` - a link to the reduced test case, if possible 51 | > 52 | > Any other information you want to share that is relevant to the issue being 53 | > reported. This might include the lines of code that you have identified as 54 | > causing the bug, and potential solutions (and your opinions on their 55 | > merits). 56 | 57 | ### Feature requests 58 | 59 | Feature requests are welcome. But take a moment to find out whether your idea 60 | fits with the scope and aims of the project. It's up to *you* to make a strong 61 | case to convince the project's developers of the merits of this feature. Please 62 | provide as much detail and context as possible. 63 | 64 | Do check if the feature request already exists. If it does, give it a thumbs-up emoji 65 | or even comment. We'd like to avoid duplicate requests. 66 | 67 | ### Pull requests 68 | 69 | Good pull requests - patches, improvements, new features - are a fantastic 70 | help. They should remain focused in scope and avoid containing unrelated 71 | commits. 72 | 73 | **Please ask first** before embarking on any significant pull request (e.g. 74 | implementing features, refactoring code, porting to a different language), 75 | otherwise you risk spending a lot of time working on something that the 76 | project's developers might not want to merge into the project. As far as _where_ to ask, 77 | the feature request or bug report is the best place to go. 78 | 79 | Please adhere to the coding conventions used throughout a project (indentation, 80 | accurate comments, etc.) and any other requirements (such as test coverage). 81 | 82 | Follow this process if you'd like your work considered for inclusion in the 83 | project: 84 | 85 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, 86 | and configure the remotes: 87 | 88 | ```bash 89 | # Clone your fork of the repo into the current directory 90 | git clone git@github.com:YOUR_USERNAME/WeScan.git 91 | # Navigate to the newly cloned directory 92 | cd WeScan 93 | # Assign the original repo to a remote called "upstream" 94 | git remote add upstream git@github.com:WeTransfer/WeScan.git 95 | ``` 96 | 97 | 2. If you cloned a while ago, get the latest changes from upstream: 98 | 99 | ```bash 100 | git checkout 101 | git pull upstream 102 | ``` 103 | 104 | 3. Create a new topic branch (off the main project development branch) to 105 | contain your feature, change, or fix: 106 | 107 | ```bash 108 | git checkout -b 109 | ``` 110 | 111 | 4. Commit your changes in logical chunks. 112 | 113 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 114 | 115 | ```bash 116 | git pull [--rebase] upstream 117 | ``` 118 | 119 | 6. Push your topic branch up to your fork: 120 | 121 | ```bash 122 | git push origin 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 | --------------------------------------------------------------------------------