The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | <?xml version="1.0" encoding="UTF-8"?>
2 | <Workspace
3 |    version = "1.0">
4 |    <FileRef
5 |       location = "self:">
6 |    </FileRef>
7 | </Workspace>
8 | 


--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/WeScan.xcscheme:
--------------------------------------------------------------------------------
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <Scheme
  3 |    LastUpgradeVersion = "1410"
  4 |    version = "1.3">
  5 |    <BuildAction
  6 |       parallelizeBuildables = "YES"
  7 |       buildImplicitDependencies = "YES">
  8 |       <BuildActionEntries>
  9 |          <BuildActionEntry
 10 |             buildForTesting = "YES"
 11 |             buildForRunning = "YES"
 12 |             buildForProfiling = "YES"
 13 |             buildForArchiving = "YES"
 14 |             buildForAnalyzing = "YES">
 15 |             <BuildableReference
 16 |                BuildableIdentifier = "primary"
 17 |                BlueprintIdentifier = "WeScan_WeScan"
 18 |                BuildableName = "WeScan_WeScan"
 19 |                BlueprintName = "WeScan_WeScan"
 20 |                ReferencedContainer = "container:">
 21 |             </BuildableReference>
 22 |          </BuildActionEntry>
 23 |          <BuildActionEntry
 24 |             buildForTesting = "YES"
 25 |             buildForRunning = "YES"
 26 |             buildForProfiling = "YES"
 27 |             buildForArchiving = "YES"
 28 |             buildForAnalyzing = "YES">
 29 |             <BuildableReference
 30 |                BuildableIdentifier = "primary"
 31 |                BlueprintIdentifier = "WeScan_WeScanTests"
 32 |                BuildableName = "WeScan_WeScanTests"
 33 |                BlueprintName = "WeScan_WeScanTests"
 34 |                ReferencedContainer = "container:">
 35 |             </BuildableReference>
 36 |          </BuildActionEntry>
 37 |          <BuildActionEntry
 38 |             buildForTesting = "YES"
 39 |             buildForRunning = "YES"
 40 |             buildForProfiling = "YES"
 41 |             buildForArchiving = "YES"
 42 |             buildForAnalyzing = "YES">
 43 |             <BuildableReference
 44 |                BuildableIdentifier = "primary"
 45 |                BlueprintIdentifier = "WeScan"
 46 |                BuildableName = "WeScan"
 47 |                BlueprintName = "WeScan"
 48 |                ReferencedContainer = "container:">
 49 |             </BuildableReference>
 50 |          </BuildActionEntry>
 51 |          <BuildActionEntry
 52 |             buildForTesting = "YES"
 53 |             buildForRunning = "YES"
 54 |             buildForProfiling = "NO"
 55 |             buildForArchiving = "NO"
 56 |             buildForAnalyzing = "YES">
 57 |             <BuildableReference
 58 |                BuildableIdentifier = "primary"
 59 |                BlueprintIdentifier = "WeScanTests"
 60 |                BuildableName = "WeScanTests"
 61 |                BlueprintName = "WeScanTests"
 62 |                ReferencedContainer = "container:">
 63 |             </BuildableReference>
 64 |          </BuildActionEntry>
 65 |       </BuildActionEntries>
 66 |    </BuildAction>
 67 |    <TestAction
 68 |       buildConfiguration = "Debug"
 69 |       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
 70 |       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
 71 |       shouldUseLaunchSchemeArgsEnv = "YES">
 72 |       <Testables>
 73 |          <TestableReference
 74 |             skipped = "NO">
 75 |             <BuildableReference
 76 |                BuildableIdentifier = "primary"
 77 |                BlueprintIdentifier = "WeScanTests"
 78 |                BuildableName = "WeScanTests"
 79 |                BlueprintName = "WeScanTests"
 80 |                ReferencedContainer = "container:">
 81 |             </BuildableReference>
 82 |          </TestableReference>
 83 |       </Testables>
 84 |    </TestAction>
 85 |    <LaunchAction
 86 |       buildConfiguration = "Debug"
 87 |       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
 88 |       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
 89 |       launchStyle = "0"
 90 |       useCustomWorkingDirectory = "NO"
 91 |       ignoresPersistentStateOnLaunch = "NO"
 92 |       debugDocumentVersioning = "YES"
 93 |       debugServiceExtension = "internal"
 94 |       allowLocationSimulation = "YES">
 95 |    </LaunchAction>
 96 |    <ProfileAction
 97 |       buildConfiguration = "Release"
 98 |       shouldUseLaunchSchemeArgsEnv = "YES"
 99 |       savedToolIdentifier = ""
100 |       useCustomWorkingDirectory = "NO"
101 |       debugDocumentVersioning = "YES">
102 |       <MacroExpansion>
103 |          <BuildableReference
104 |             BuildableIdentifier = "primary"
105 |             BlueprintIdentifier = "WeScan_WeScan"
106 |             BuildableName = "WeScan_WeScan"
107 |             BlueprintName = "WeScan_WeScan"
108 |             ReferencedContainer = "container:">
109 |          </BuildableReference>
110 |       </MacroExpansion>
111 |    </ProfileAction>
112 |    <AnalyzeAction
113 |       buildConfiguration = "Debug">
114 |    </AnalyzeAction>
115 |    <ArchiveAction
116 |       buildConfiguration = "Release"
117 |       revealArchiveInOrganizer = "YES">
118 |    </ArchiveAction>
119 | </Scheme>
120 | 


--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/avanderlee.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 | <dict>
 5 | 	<key>SchemeUserState</key>
 6 | 	<dict>
 7 | 		<key>WeScan.xcscheme_^#shared#^_</key>
 8 | 		<dict>
 9 | 			<key>orderHint</key>
10 | 			<integer>0</integer>
11 | 		</dict>
12 | 	</dict>
13 | </dict>
14 | </plist>
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** &mdash; check if the issue has already been
 28 |    reported.
 29 | 
 30 | 2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
 31 |    latest `master` or development branch in the repository.
 32 | 
 33 | 3. **Isolate the problem** &mdash; 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 | > `<url>` - 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 <dev-branch>
101 |    git pull upstream <dev-branch>
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 <topic-branch-name>
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 <dev-branch>
117 |    ```
118 | 
119 | 6. Push your topic branch up to your fork:
120 | 
121 |    ```bash
122 |    git push origin <topic-branch-name>
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 | <?xml version="1.0" encoding="UTF-8"?>
2 | <Workspace
3 |    version = "1.0">
4 |    <FileRef
5 |       location = "self:">
6 |    </FileRef>
7 | </Workspace>
8 | 


--------------------------------------------------------------------------------
/Example/WeScan.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3 | <plist version="1.0">
4 | <dict>
5 | 	<key>IDEDidComputeMac32BitWarning</key>
6 | 	<true/>
7 | </dict>
8 | </plist>
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 | <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 2 | <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
 3 |     <dependencies>
 4 |         <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
 5 |         <capability name="Safe area layout guides" minToolsVersion="9.0"/>
 6 |         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
 7 |     </dependencies>
 8 |     <scenes>
 9 |         <!--View Controller-->
10 |         <scene sceneID="EHf-IW-A2E">
11 |             <objects>
12 |                 <viewController id="01J-lp-oVM" sceneMemberID="viewController">
13 |                     <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
14 |                         <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
15 |                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
16 |                         <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
17 |                         <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
18 |                     </view>
19 |                 </viewController>
20 |                 <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
21 |             </objects>
22 |             <point key="canvasLocation" x="53" y="375"/>
23 |         </scene>
24 |     </scenes>
25 | </document>
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 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 | <dict>
 5 | 	<key>CFBundleDevelopmentRegion</key>
 6 | 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 7 | 	<key>CFBundleExecutable</key>
 8 | 	<string>$(EXECUTABLE_NAME)</string>
 9 | 	<key>CFBundleIdentifier</key>
10 | 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11 | 	<key>CFBundleInfoDictionaryVersion</key>
12 | 	<string>6.0</string>
13 | 	<key>CFBundleName</key>
14 | 	<string>$(PRODUCT_NAME)</string>
15 | 	<key>CFBundlePackageType</key>
16 | 	<string>APPL</string>
17 | 	<key>CFBundleShortVersionString</key>
18 | 	<string>1.0</string>
19 | 	<key>CFBundleVersion</key>
20 | 	<string>1</string>
21 | 	<key>LSRequiresIPhoneOS</key>
22 | 	<true/>
23 | 	<key>NSCameraUsageDescription</key>
24 | 	<string>Used to scan images.</string>
25 | 	<key>NSPhotoLibraryUsageDescription</key>
26 | 	<string>Used to select images from your Photo Library.</string>
27 | 	<key>UILaunchStoryboardName</key>
28 | 	<string>LaunchScreen</string>
29 | 	<key>UIMainStoryboardFile</key>
30 | 	<string>Main</string>
31 | 	<key>UIRequiredDeviceCapabilities</key>
32 | 	<array>
33 | 		<string>armv7</string>
34 | 	</array>
35 | 	<key>UISupportedInterfaceOrientations</key>
36 | 	<array>
37 | 		<string>UIInterfaceOrientationPortrait</string>
38 | 		<string>UIInterfaceOrientationLandscapeLeft</string>
39 | 		<string>UIInterfaceOrientationLandscapeRight</string>
40 | 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
41 | 	</array>
42 | 	<key>UISupportedInterfaceOrientations~ipad</key>
43 | 	<array>
44 | 		<string>UIInterfaceOrientationPortrait</string>
45 | 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
46 | 		<string>UIInterfaceOrientationLandscapeLeft</string>
47 | 		<string>UIInterfaceOrientationLandscapeRight</string>
48 | 	</array>
49 | </dict>
50 | </plist>
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 | <p align="center">
  4 |     <img width="900px" src="Assets/WeScan-Banner.jpg">
  5 | </p>
  6 | 
  7 | <p align="center">
  8 | <img src="https://app.bitrise.io/app/df00af454f27891d.svg?token=spjxNvzjnRqug6GfGM3_Lg"/>
  9 | <img src="https://img.shields.io/cocoapods/l/WeScan.svg?style=flat"/>
 10 | <img src="https://img.shields.io/cocoapods/p/WeScan.svg?style=flat"/>
 11 | <img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"/>
 12 | <img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=flat"/>
 13 | </p>
 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 | <p align="left">
 39 |     <img width="350px" src="Assets/WeScan.gif">
 40 | </p>
 41 | 
 42 | ## Requirements
 43 | 
 44 | - Swift 5.0
 45 | - iOS 10.0+
 46 | 
 47 | <br>
 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 <yourProjectName.swift.h>`)
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 | <br>
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 | <br>
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<UnitAngle>(value: 90, unit: .degrees)
146 |         reloadImage(withAngle: rotationAngle)
147 |     }
148 | 
149 |     private func reloadImage(withAngle angle: Measurement<UnitAngle>) {
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<UnitAngle>, 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<Data, ImageScannerError>) -> 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<UnitAngle>) {
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<UnitAngle>(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<UITouch>, 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 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3 | <plist version="1.0">
 4 | <dict>
 5 | 	<key>CFBundleDevelopmentRegion</key>
 6 | 	<string>$(DEVELOPMENT_LANGUAGE)</string>
 7 | 	<key>CFBundleExecutable</key>
 8 | 	<string>$(EXECUTABLE_NAME)</string>
 9 | 	<key>CFBundleIdentifier</key>
10 | 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11 | 	<key>CFBundleInfoDictionaryVersion</key>
12 | 	<string>6.0</string>
13 | 	<key>CFBundleName</key>
14 | 	<string>$(PRODUCT_NAME)</string>
15 | 	<key>CFBundlePackageType</key>
16 | 	<string>BNDL</string>
17 | 	<key>CFBundleShortVersionString</key>
18 | 	<string>1.0</string>
19 | 	<key>CFBundleVersion</key>
20 | 	<string>1</string>
21 | </dict>
22 | </plist>
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 | 


--------------------------------------------------------------------------------