From e8ef54c3a3cafefb508a1958cf2cc8ff1d3b4651 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 17 Apr 2024 11:14:12 +0200 Subject: [PATCH 1/8] feat: add android scanner mode option --- README.md | 9 ++-- .../CunningDocumentScannerPlugin.kt | 7 +-- example/lib/main.dart | 13 ++++-- lib/cunning_document_scanner.dart | 46 ++++++++++++++++--- 4 files changed, 60 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 19a9ed3..f159a12 100644 --- a/README.md +++ b/README.md @@ -95,9 +95,12 @@ There are some features in Android that allow you to adjust the scanner that wil ``` final imagesPath = await CunningDocumentScanner.getPictures( - noOfPages: 1, // Limit the number of pages to 1 - isGalleryImportAllowed, // Allow the user to also pick an image from his gallery - ) + androidOptions: const AndroidScannerOptions( + noOfPages: 1, // Limit the number of pages to 1 + isGalleryImportAllowed: true, // Allow the user to also pick an image from his gallery + scannerMode: AndroidScannerMode.scannerModeFull, + ) + ) ``` ## Installation diff --git a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt index ef6589a..b13f9ec 100644 --- a/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt +++ b/android/src/main/kotlin/biz/cunning/cunning_document_scanner/CunningDocumentScannerPlugin.kt @@ -48,8 +48,9 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA if (call.method == "getPictures") { val noOfPages = call.argument("noOfPages") ?: 50; val isGalleryImportAllowed = call.argument("isGalleryImportAllowed") ?: false; + val scannerMode = call.argument("scannerMode") ?: 1; this.pendingResult = result - startScan(noOfPages, isGalleryImportAllowed) + startScan(noOfPages, isGalleryImportAllowed, scannerMode) } else { result.notImplemented() } @@ -168,12 +169,12 @@ class CunningDocumentScannerPlugin : FlutterPlugin, MethodCallHandler, ActivityA /** * add document scanner result handler and launch the document scanner */ - private fun startScan(noOfPages: Int, isGalleryImportAllowed: Boolean) { + private fun startScan(noOfPages: Int, isGalleryImportAllowed: Boolean,scannerMode: Int) { val options = GmsDocumentScannerOptions.Builder() .setGalleryImportAllowed(isGalleryImportAllowed) .setPageLimit(noOfPages) .setResultFormats(RESULT_FORMAT_JPEG) - .setScannerMode(SCANNER_MODE_FULL) + .setScannerMode(scannerMode) .build() val scanner = GmsDocumentScanning.getClient(options) scanner.getStartScanIntent(activity).addOnSuccessListener { diff --git a/example/lib/main.dart b/example/lib/main.dart index 69e5cd8..7979f29 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,8 @@ -import 'dart:io'; -import 'package:flutter/material.dart'; import 'dart:async'; +import 'dart:io'; + import 'package:cunning_document_scanner/cunning_document_scanner.dart'; +import 'package:flutter/material.dart'; void main() { runApp(const MyApp()); @@ -48,7 +49,13 @@ class _MyAppState extends State { void onPressed() async { List pictures; try { - pictures = await CunningDocumentScanner.getPictures() ?? []; + pictures = await CunningDocumentScanner.getPictures( + androidOptions: const AndroidScannerOptions( + scannerMode: AndroidScannerMode.scannerModeBase, + isGalleryImportAllowed: true, + ), + ) ?? + []; if (!mounted) return; setState(() { _pictures = pictures; diff --git a/lib/cunning_document_scanner.dart b/lib/cunning_document_scanner.dart index 7570739..e2a4ca5 100644 --- a/lib/cunning_document_scanner.dart +++ b/lib/cunning_document_scanner.dart @@ -1,15 +1,43 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; +class AndroidScannerOptions { + const AndroidScannerOptions({ + this.isGalleryImportAllowed = false, + this.scannerMode = AndroidScannerMode.scannerModeFull, + this.noOfPages = 100, + }); + + final bool isGalleryImportAllowed; + final AndroidScannerMode scannerMode; + final int noOfPages; +} + +enum AndroidScannerMode { + scannerModeFull( + 1), // adds ML-enabled image cleaning capabilities (erase stains, fingers, etc…) to the SCANNER_MODE_BASE_WITH_FILTER mode. + scannerModeBaseWithFilter( + 2), // Adds image filters (grayscale, auto image enhancement, etc…) to the SCANNER_MODE_BASE mode. + scannerModeBase( + 3); // basic editing capabilities (crop, rotate, reorder pages, etc…). + + const AndroidScannerMode(this.value); + final int value; +} + class CunningDocumentScanner { + static const _defaultAndroidOptions = AndroidScannerOptions(); + static const MethodChannel _channel = MethodChannel('cunning_document_scanner'); /// Call this to start get Picture workflow. - static Future?> getPictures( - {int noOfPages = 100, bool isGalleryImportAllowed = false}) async { + static Future?> getPictures({ + AndroidScannerOptions androidOptions = _defaultAndroidOptions, + }) async { Map statuses = await [ Permission.camera, ].request(); @@ -18,10 +46,16 @@ class CunningDocumentScanner { throw Exception("Permission not granted"); } - final List? pictures = await _channel.invokeMethod('getPictures', { - 'noOfPages': noOfPages, - 'isGalleryImportAllowed': isGalleryImportAllowed - }); + List? pictures; + if (Platform.isAndroid) { + pictures = await _channel.invokeMethod('getPictures', { + 'noOfPages': androidOptions.noOfPages, + 'isGalleryImportAllowed': androidOptions.isGalleryImportAllowed, + 'scannerMode': androidOptions.scannerMode.value, + }); + } else if (Platform.isIOS) { + pictures = await _channel.invokeMethod('getPictures', {}); + } return pictures?.map((e) => e as String).toList(); } } From 432b52a6d161335dc0c73d5562e230e640b9d57c Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 18 Apr 2024 10:57:26 +0200 Subject: [PATCH 2/8] add WeScan support --- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 10 +- example/ios/Runner/Base.lproj/Main.storyboard | 13 +- example/lib/main.dart | 3 + .../DocumentScannerStoryboard.storyboard | 228 +++++++++++ .../Controllers/EditImageViewController.swift | 59 +++ .../ReviewImageViewController.swift | 36 ++ .../ScanCameraViewController.swift | 116 ++++++ ios/Classes/DocumentPhotoScanController.swift | 137 +++++++ .../SwiftCunningDocumentScannerPlugin.swift | 75 ++-- ios/Classes/Utils/FileUtils.swift | 55 +++ ios/WeScan-3.0.0/CODEOWNERS | 5 + ios/WeScan-3.0.0/CODE_OF_CONDUCT.md | 46 +++ ios/WeScan-3.0.0/CONTRIBUTING.md | 164 ++++++++ ios/WeScan-3.0.0/Changelog.md | 80 ++++ ios/WeScan-3.0.0/LICENSE | 21 + ios/WeScan-3.0.0/Package.resolved | 14 + ios/WeScan-3.0.0/Package.swift | 35 ++ ios/WeScan-3.0.0/README.md | 136 +++++++ .../WeScan/Common/CIRectangleDetector.swift | 41 ++ .../WeScan/Common/EditScanCornerView.swift | 75 ++++ .../Sources/WeScan/Common/Error.swift | 39 ++ .../Sources/WeScan/Common/Quadrilateral.swift | 239 ++++++++++++ .../WeScan/Common/QuadrilateralView.swift | 314 +++++++++++++++ .../Common/VisionRectangleDetector.swift | 99 +++++ .../WeScan/Edit/EditImageViewController.swift | 205 ++++++++++ .../WeScan/Edit/EditScanViewController.swift | 230 +++++++++++ .../WeScan/Edit/ZoomGestureController.swift | 64 ++++ .../AVCaptureVideoOrientation+Utils.swift | 36 ++ .../WeScan/Extensions/Array+Utils.swift | 23 ++ .../Extensions/CGAffineTransform+Utils.swift | 37 ++ .../CGImagePropertyOrientation.swift | 36 ++ .../WeScan/Extensions/CGPoint+Utils.swift | 70 ++++ .../WeScan/Extensions/CGRect+Utils.swift | 31 ++ .../WeScan/Extensions/CGSize+Utils.swift | 27 ++ .../WeScan/Extensions/CIImage+Utils.swift | 40 ++ .../Extensions/UIImage+Orientation.swift | 99 +++++ .../WeScan/Extensions/UIImage+SFSymbol.swift | 27 ++ .../WeScan/Extensions/UIImage+Utils.swift | 147 +++++++ .../WeScan/ImageScannerController.swift | 214 +++++++++++ .../WeScan/Protocols/CaptureDevice.swift | 62 +++ .../WeScan/Protocols/Transformable.swift | 42 ++ .../WeScan/Resources/Assets/enhance.png | Bin 0 -> 511 bytes .../WeScan/Resources/Assets/enhance@2x.png | Bin 0 -> 934 bytes .../WeScan/Resources/Assets/enhance@3x.png | Bin 0 -> 1332 bytes .../Sources/WeScan/Resources/Assets/flash.png | Bin 0 -> 338 bytes .../WeScan/Resources/Assets/flash@2x.png | Bin 0 -> 555 bytes .../WeScan/Resources/Assets/flash@3x.png | Bin 0 -> 798 bytes .../Resources/Assets/flashUnavailable.png | Bin 0 -> 395 bytes .../Resources/Assets/flashUnavailable@2x.png | Bin 0 -> 669 bytes .../Resources/Assets/flashUnavailable@3x.png | Bin 0 -> 940 bytes .../WeScan/Resources/Assets/rotate.png | Bin 0 -> 692 bytes .../WeScan/Resources/Assets/rotate@2x.png | Bin 0 -> 1006 bytes .../WeScan/Resources/Assets/rotate@3x.png | Bin 0 -> 1318 bytes .../Localisation/ar.lproj/Localizable.strings | 21 + .../Localisation/cs.lproj/Localizable.strings | 21 + .../Localisation/de.lproj/Localizable.strings | 21 + .../Localisation/en.lproj/Localizable.strings | 21 + .../es-419.lproj/Localizable.strings | 21 + .../Localisation/es.lproj/Localizable.strings | 21 + .../Localisation/fr.lproj/Localizable.strings | 21 + .../Localisation/hu.lproj/Localizable.strings | 21 + .../Localisation/it.lproj/Localizable.strings | 21 + .../Localisation/ko.lproj/Localizable.strings | 21 + .../Localisation/nl.lproj/Localizable.strings | 21 + .../Localisation/pl.lproj/Localizable.strings | 21 + .../pt-BR.lproj/Localizable.strings | 22 ++ .../pt-PT.lproj/Localizable.strings | 21 + .../Localisation/ru.lproj/Localizable.strings | 21 + .../Localisation/sv.lproj/Localizable.strings | 21 + .../Localisation/tr.lproj/Localizable.strings | 21 + .../zh-Hans.lproj/Localizable.strings | 21 + .../zh-Hant.lproj/Localizable.strings | 21 + .../WeScan/Review/ReviewViewController.swift | 183 +++++++++ .../Scan/CameraScannerViewController.swift | 201 ++++++++++ .../WeScan/Scan/CaptureSessionManager.swift | 362 ++++++++++++++++++ .../WeScan/Scan/FocusRectangleView.swift | 46 +++ .../WeScan/Scan/RectangleFeaturesFunnel.swift | 190 +++++++++ .../WeScan/Scan/ScannerViewController.swift | 325 ++++++++++++++++ .../Sources/WeScan/Scan/ShutterButton.swift | 106 +++++ .../WeScan/Session/CaptureSession+Flash.swift | 45 +++ .../WeScan/Session/CaptureSession+Focus.swift | 73 ++++ .../Session/CaptureSession+Orientation.swift | 60 +++ .../WeScan/Session/CaptureSession.swift | 37 ++ .../WeScan/ja.lproj/Localizable.strings | 22 ++ ios/WeScan-3.0.0/WeScan.podspec | 20 + ios/cunning_document_scanner.podspec | 2 + lib/cunning_document_scanner.dart | 17 +- 88 files changed, 5464 insertions(+), 57 deletions(-) create mode 100644 ios/Base.lproj/DocumentScannerStoryboard.storyboard create mode 100644 ios/Classes/Controllers/EditImageViewController.swift create mode 100644 ios/Classes/Controllers/ReviewImageViewController.swift create mode 100644 ios/Classes/Controllers/ScanCameraViewController.swift create mode 100644 ios/Classes/DocumentPhotoScanController.swift create mode 100644 ios/Classes/Utils/FileUtils.swift create mode 100644 ios/WeScan-3.0.0/CODEOWNERS create mode 100755 ios/WeScan-3.0.0/CODE_OF_CONDUCT.md create mode 100644 ios/WeScan-3.0.0/CONTRIBUTING.md create mode 100644 ios/WeScan-3.0.0/Changelog.md create mode 100755 ios/WeScan-3.0.0/LICENSE create mode 100644 ios/WeScan-3.0.0/Package.resolved create mode 100644 ios/WeScan-3.0.0/Package.swift create mode 100644 ios/WeScan-3.0.0/README.md create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/CIRectangleDetector.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/EditScanCornerView.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/Error.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/Quadrilateral.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/QuadrilateralView.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Common/VisionRectangleDetector.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Edit/EditImageViewController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Edit/EditScanViewController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Edit/ZoomGestureController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/Array+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGAffineTransform+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGImagePropertyOrientation.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGPoint+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGRect+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGSize+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/CIImage+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Orientation.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+SFSymbol.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Utils.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/ImageScannerController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Protocols/CaptureDevice.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Protocols/Transformable.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance@2x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance@3x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@2x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@3x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@2x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@3x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@2x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@3x.png create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ar.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/cs.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/de.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/en.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es-419.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/fr.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/hu.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/it.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ko.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/nl.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pl.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-BR.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-PT.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ru.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/sv.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/tr.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hans.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hant.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Review/ReviewViewController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/CameraScannerViewController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/CaptureSessionManager.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/FocusRectangleView.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/RectangleFeaturesFunnel.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/ScannerViewController.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Scan/ShutterButton.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Flash.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Focus.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Orientation.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession.swift create mode 100644 ios/WeScan-3.0.0/Sources/WeScan/ja.lproj/Localizable.strings create mode 100644 ios/WeScan-3.0.0/WeScan.podspec diff --git a/example/ios/Podfile b/example/ios/Podfile index cdaa6d6..f494ceb 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -30,7 +30,7 @@ flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! - + pod 'WeScan', :path => '.symlinks/plugins/cunning_document_scanner/ios/WeScan-3.0.0' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 4f954af..23fa47d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,18 +1,21 @@ PODS: - cunning_document_scanner (1.0.0): - Flutter + - WeScan - Flutter (1.0.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - WeScan (3.0.0) DEPENDENCIES: - cunning_document_scanner (from `.symlinks/plugins/cunning_document_scanner/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - WeScan (from `.symlinks/plugins/cunning_document_scanner/ios/WeScan-3.0.0`) EXTERNAL SOURCES: cunning_document_scanner: @@ -23,13 +26,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + WeScan: + :path: ".symlinks/plugins/cunning_document_scanner/ios/WeScan-3.0.0" SPEC CHECKSUMS: - cunning_document_scanner: 7cb9bd173f7cc7b11696dde98d01492187fc3a67 + cunning_document_scanner: 92b962b74cb33460ccb4a27c8154a9fc9d37f24c Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 + WeScan: f50277f7304d146d9ebc4ebdc1b587df84dcdd8b -PODFILE CHECKSUM: e78c989774f3b5b54daf69ce13097109fe4b0da3 +PODFILE CHECKSUM: c1a4e03013e58f8bd901ec6ecbff4df9feb094cc COCOAPODS: 1.15.2 diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard index f3c2851..228e24f 100644 --- a/example/ios/Runner/Base.lproj/Main.storyboard +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 7979f29..b562ec0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,6 +54,9 @@ class _MyAppState extends State { scannerMode: AndroidScannerMode.scannerModeBase, isGalleryImportAllowed: true, ), + iOSOptions: const IOSScannerOptions( + isGalleryImportAllowed: true, + ), ) ?? []; if (!mounted) return; diff --git a/ios/Base.lproj/DocumentScannerStoryboard.storyboard b/ios/Base.lproj/DocumentScannerStoryboard.storyboard new file mode 100644 index 0000000..f3e06d6 --- /dev/null +++ b/ios/Base.lproj/DocumentScannerStoryboard.storyboard @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Classes/Controllers/EditImageViewController.swift b/ios/Classes/Controllers/EditImageViewController.swift new file mode 100644 index 0000000..7902bd7 --- /dev/null +++ b/ios/Classes/Controllers/EditImageViewController.swift @@ -0,0 +1,59 @@ +// +// EditScanViewController.swift +// cunning_document_scanner +// +// Created by Romain Boucher on 18/04/2024. +// + +import Foundation +import WeScan + +class EditImageViewController : UIViewController{ + @IBOutlet private weak var editImageView: UIView! + var result: FlutterResult!; + var captureImage: UIImage!; + var quad: Quadrilateral? + var controller: WeScan.EditImageViewController! + + public func setParams(result: @escaping FlutterResult, image: UIImage!, quad: Quadrilateral?) { + self.result = result + self.captureImage = image + self.quad = quad + } + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + private func setupView() { + controller = WeScan.EditImageViewController( + image: captureImage, + quad: quad, + strokeColor: UIColor(red: (69.0 / 255.0), green: (194.0 / 255.0), blue: (177.0 / 255.0), alpha: 1.0).cgColor + ) + controller.view.frame = editImageView.bounds + controller.willMove(toParent: self) + editImageView.addSubview(controller.view) + self.addChild(controller) + controller.didMove(toParent: self) + controller.delegate = self + } + + @IBAction func cropTapped(_ sender: UIButton!) { + controller.cropImage() + } + + func pushReviewImageViewController(image: UIImage){ + guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "ReviewImageViewController") as? ReviewImageViewController + else { return } + controller.setParams(result: result, image: image) + navigationController?.pushViewController(controller, animated: false) + } +} + +extension EditImageViewController: EditImageViewDelegate { + func cropped(image: UIImage) { + pushReviewImageViewController(image: image) + } +} diff --git a/ios/Classes/Controllers/ReviewImageViewController.swift b/ios/Classes/Controllers/ReviewImageViewController.swift new file mode 100644 index 0000000..ea51011 --- /dev/null +++ b/ios/Classes/Controllers/ReviewImageViewController.swift @@ -0,0 +1,36 @@ +// +// ReviewImageViewController.swift +// cunning_document_scanner +// +// Created by Romain Boucher on 18/04/2024. +// + +import Foundation + +final class ReviewImageViewController: UIViewController { + + @IBOutlet private weak var imageView: UIImageView! + var image: UIImage! + var result: FlutterResult! + + public func setParams(result: @escaping FlutterResult, image: UIImage!) { + self.result = result + self.image = image + } + + override func viewDidLoad() { + super.viewDidLoad() + imageView.image = image + } + + @IBAction func sendTapped(_ sender: UIButton) { + let filePath = FileUtils.saveImage(image: image) + if(filePath != nil){ + result([filePath]) + }else{ + result(nil) + } + self.dismiss(animated: true) + } + +} diff --git a/ios/Classes/Controllers/ScanCameraViewController.swift b/ios/Classes/Controllers/ScanCameraViewController.swift new file mode 100644 index 0000000..dbd5936 --- /dev/null +++ b/ios/Classes/Controllers/ScanCameraViewController.swift @@ -0,0 +1,116 @@ +// +// NewCameraViewController.swift +// WeScanSampleProject +// +// Created by Chawatvish Worrapoj on 7/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit +import WeScan + +final class ScanCameraViewController: UIViewController { + + @IBOutlet private weak var cameraView: UIView! + @IBOutlet private weak var bottomView: UIView! + @IBOutlet private weak var galleryButton: UIButton! + @IBOutlet private weak var autoModeButton: UIButton! + var controller: CameraScannerViewController! + var isGalleryImportAllowed: Bool = false + var isAutoScanEnabled: Bool = true + var result: FlutterResult!; + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + public func setParams(result: @escaping FlutterResult, isGalleryImportAllowed: Bool = false, isAutoScanEnabled: Bool = true) { + self.result = result + self.isGalleryImportAllowed = isGalleryImportAllowed + self.isAutoScanEnabled = isAutoScanEnabled + } + + private func setupView() { + navigationController?.interactivePopGestureRecognizer?.isEnabled = false + + controller = CameraScannerViewController() + controller.modalPresentationStyle = .fullScreen + controller.view.frame = cameraView.bounds + controller.willMove(toParent: self) + cameraView.addSubview(controller.view) + self.addChild(controller) + controller.didMove(toParent: self) + controller.delegate = self + controller.isAutoScanEnabled = isAutoScanEnabled; + galleryButton.isHidden = !isGalleryImportAllowed + + setAutoModeButtonView() + } + + private func setAutoModeButtonView() { + if(controller.isAutoScanEnabled) { + autoModeButton.setTitle("Auto", for: .normal) + } else { + autoModeButton.setTitle("Manual", for: .normal) + } + } + + @IBAction func flashTapped(_ sender: UIButton) { + controller.toggleFlash() + } + + @IBAction func autoModeTapped(_ sender: UIButton) { + controller.isAutoScanEnabled = !controller.isAutoScanEnabled + setAutoModeButtonView() + } + + @IBAction func captureTapped(_ sender: UIButton) { + controller.capture() + } + + @IBAction func cancelTapped(_ sender: UIButton) { + result(nil) + self.dismiss(animated: true) + } + + @IBAction func galleryTapped(_ sender: UIButton) { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + + present(imagePicker, animated: true) + } + + func pushEditDocumentViewController(image: UIImage, quad: Quadrilateral?){ + guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "EditImageViewController") as? EditImageViewController + else { return } + controller.setParams(result: result, image: image, quad: quad) + navigationController?.pushViewController(controller, animated: false) + } +} + +extension ScanCameraViewController: CameraScannerViewOutputDelegate { + func captureImageFailWithError(error: Error) { + print(error) + } + + func captureImageSuccess(image: UIImage, withQuad quad: Quadrilateral?) { + pushEditDocumentViewController(image: image, quad: quad) + } +} + + +extension ScanCameraViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{ + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + picker.dismiss(animated: true) + + guard let image = info[.originalImage] as? UIImage else { return } + + pushEditDocumentViewController(image: image, quad: nil) + } +} diff --git a/ios/Classes/DocumentPhotoScanController.swift b/ios/Classes/DocumentPhotoScanController.swift new file mode 100644 index 0000000..e1da231 --- /dev/null +++ b/ios/Classes/DocumentPhotoScanController.swift @@ -0,0 +1,137 @@ +// +// ScanPhotoViewController.swift +// edge_detection +// +// Created by Henry Leung on 3/9/2021. +// + +import WeScan +import Flutter +import Foundation + +class DocumentPhotoScanController: UIViewController, ImageScannerControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + var _result:FlutterResult? + + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + picker.dismiss(animated: true) + + _result!(nil) + dismiss(animated: true) + } + + public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + picker.dismiss(animated: true) + + guard let image = info[.originalImage] as? UIImage else { return } + let scannerVC = ImageScannerController(image: image) + scannerVC.imageScannerDelegate = self + + if #available(iOS 13.0, *) { + scannerVC.isModalInPresentation = true + scannerVC.overrideUserInterfaceStyle = .dark + scannerVC.view.backgroundColor = .black + } + present(scannerVC, animated: true) + } + + override func viewDidAppear(_ animated: Bool) { + + // Temp fix for https://github.com/WeTransfer/WeScan/issues/320 + if #available(iOS 15, *) { + let appearance = UINavigationBarAppearance() + let navigationBar = UINavigationBar() + appearance.configureWithOpaqueBackground() + appearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label] + appearance.backgroundColor = .systemBackground + navigationBar.standardAppearance = appearance; + UINavigationBar.appearance().scrollEdgeAppearance = appearance + + let appearanceTB = UITabBarAppearance() + appearanceTB.configureWithOpaqueBackground() + appearanceTB.backgroundColor = .systemBackground + UITabBar.appearance().standardAppearance = appearanceTB + UITabBar.appearance().scrollEdgeAppearance = appearanceTB + } + + if self.isBeingPresented { + let imagePicker = UIImagePickerController() + imagePicker.delegate = self + imagePicker.sourceType = .photoLibrary + + present(imagePicker, animated: true) + } + } + + func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { + print(error) + _result!(nil) + self.dismiss(animated: true) + } + + func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { + // Your ViewController is responsible for dismissing the ImageScannerController + scanner.dismiss(animated: true) + + + let filePath = saveImage(image:results.doesUserPreferEnhancedScan ? results.enhancedScan!.image : results.croppedScan.image) + var filenames: [String] = [] + if(filePath != nil){ + filenames.append(filePath!) + } + _result!(filenames) + self.dismiss(animated: true) + } + + + func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { + // Your ViewController is responsible for dismissing the ImageScannerController + scanner.dismiss(animated: true) + _result!(nil) + self.dismiss(animated: true) + } + + + func saveImage(image: UIImage) -> String? { + guard let data = image.pngData() else { + return nil + } + + let tempDirPath = getDocumentsDirectory() + let currentDateTime = Date() + let df = DateFormatter() + df.dateFormat = "yyyyMMdd-HHmmss" + let formattedDate = df.string(from: currentDateTime) + let filePath = tempDirPath.appendingPathComponent(formattedDate + ".png") + + do { + let fileManager = FileManager.default + + // Check if file exists + if fileManager.fileExists(atPath: filePath.path) { + // Delete file + try fileManager.removeItem(atPath: filePath.path) + } else { + print("File does not exist") + } + + } + catch let error as NSError { + print("An error took place: \(error)") + } + + do { + try data.write(to: filePath) + return filePath.path + } catch { + print(error.localizedDescription) + return nil + } + } + + func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory + } +} diff --git a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift index e1379c0..95aedc4 100644 --- a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift +++ b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift @@ -4,10 +4,7 @@ import Vision import VisionKit @available(iOS 13.0, *) -public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin, VNDocumentCameraViewControllerDelegate { - var resultChannel :FlutterResult? - var presentingController: VNDocumentCameraViewController? - +public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "cunning_document_scanner", binaryMessenger: registrar.messenger()) let instance = SwiftCunningDocumentScannerPlugin() @@ -15,49 +12,31 @@ public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin, VNDocum } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - if call.method == "getPictures" { - let presentedVC: UIViewController? = UIApplication.shared.keyWindow?.rootViewController - self.resultChannel = result - self.presentingController = VNDocumentCameraViewController() - self.presentingController!.delegate = self - presentedVC?.present(self.presentingController!, animated: true) - } else { - result(FlutterMethodNotImplemented) - return - } - } - - - func getDocumentsDirectory() -> URL { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - let documentsDirectory = paths[0] - return documentsDirectory - } - - public func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFinishWith scan: VNDocumentCameraScan) { - let tempDirPath = self.getDocumentsDirectory() - let currentDateTime = Date() - let df = DateFormatter() - df.dateFormat = "yyyyMMdd-HHmmss" - let formattedDate = df.string(from: currentDateTime) - var filenames: [String] = [] - for i in 0 ... scan.pageCount - 1 { - let page = scan.imageOfPage(at: i) - let url = tempDirPath.appendingPathComponent(formattedDate + "-\(i).png") - try? page.pngData()?.write(to: url) - filenames.append(url.path) - } - resultChannel?(filenames) - presentingController?.dismiss(animated: true) - } - - public func documentCameraViewControllerDidCancel(_ controller: VNDocumentCameraViewController) { - resultChannel?(nil) - presentingController?.dismiss(animated: true) - } - - public func documentCameraViewController(_ controller: VNDocumentCameraViewController, didFailWithError error: Error) { - resultChannel?(nil) - presentingController?.dismiss(animated: true) + let args = call.arguments as! Dictionary + let isGalleryImportAllowed = args["isGalleryImportAllowed"] as? Bool ?? false + let isAutoScanEnabled = args["isAutoScanEnabled"] as? Bool ?? false + + if (call.method == "getPictures") + { + if let viewController = UIApplication.shared.delegate?.window??.rootViewController as? FlutterViewController { + let storyboard = UIStoryboard(name: "DocumentScannerStoryboard", bundle: Bundle(for: CunningDocumentScannerPlugin.self)) + guard let controller = storyboard.instantiateInitialViewController() as? UINavigationController else { + result(nil) + return + } + controller.modalPresentationStyle = .fullScreen + (controller.viewControllers.first as? ScanCameraViewController)?.setParams( + result: result, + isGalleryImportAllowed: isGalleryImportAllowed, + isAutoScanEnabled: isAutoScanEnabled + ) + + viewController.present(controller, animated: true, completion: nil) + } else{ + result(nil) + } + }else{ + result(FlutterMethodNotImplemented) } + } } diff --git a/ios/Classes/Utils/FileUtils.swift b/ios/Classes/Utils/FileUtils.swift new file mode 100644 index 0000000..143135a --- /dev/null +++ b/ios/Classes/Utils/FileUtils.swift @@ -0,0 +1,55 @@ +// +// FolderUtils.swift +// cunning_document_scanner +// +// Created by Romain Boucher on 18/04/2024. +// + +import Foundation + +class FileUtils{ + static func getDocumentsDirectory() -> URL { + let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let documentsDirectory = paths[0] + return documentsDirectory + } + + static func saveImage(image: UIImage) -> String? { + guard let data = image.pngData() else { + return nil + } + + let tempDirPath = getDocumentsDirectory() + let currentDateTime = Date() + let df = DateFormatter() + df.dateFormat = "yyyyMMdd-HHmmss" + let formattedDate = df.string(from: currentDateTime) + let filePath = tempDirPath.appendingPathComponent(formattedDate + ".png") + + do { + let fileManager = FileManager.default + // Check if file exists + if fileManager.fileExists(atPath: filePath.path) { + // Delete file + try fileManager.removeItem(atPath: filePath.path) + } + else { + print("File does not exist") + } + } + catch let error as NSError { + print("An error took place: \(error)") + } + + do { + try data.write(to: filePath) + return filePath.path + } + + catch { + print(error.localizedDescription) + return nil + } + } + +} diff --git a/ios/WeScan-3.0.0/CODEOWNERS b/ios/WeScan-3.0.0/CODEOWNERS new file mode 100644 index 0000000..dfb9769 --- /dev/null +++ b/ios/WeScan-3.0.0/CODEOWNERS @@ -0,0 +1,5 @@ +# https://help.github.com/en/articles/about-code-owners +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, they +# will be requested for review when someone opens a PR. +* @kairadiagne @AvdLee @BasThomas @peagasilva @neodym diff --git a/ios/WeScan-3.0.0/CODE_OF_CONDUCT.md b/ios/WeScan-3.0.0/CODE_OF_CONDUCT.md new file mode 100755 index 0000000..ee4b7d5 --- /dev/null +++ b/ios/WeScan-3.0.0/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +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. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +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. + +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. + +## Scope + +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. + +## Enforcement + +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. + +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. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/ios/WeScan-3.0.0/CONTRIBUTING.md b/ios/WeScan-3.0.0/CONTRIBUTING.md new file mode 100644 index 0000000..f1c18e6 --- /dev/null +++ b/ios/WeScan-3.0.0/CONTRIBUTING.md @@ -0,0 +1,164 @@ +# Contributing to WeScan + +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. + +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. + +In general, we expect you to follow our [Code of Conduct](https://github.com/WeTransfer/WeScan/blob/master/CODE_OF_CONDUCT.md). + +## Using Github Issues + +### First time contributors +We should encourage first time contributors. A good inspiration on this can be found [here](http://www.firsttimersonly.com/). As pointed out: + +> 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: + +> "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!" + +By labeling issues with this `first-timers-only` label we help first time contributors step up their game and start contributing. + +### Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest `master` or development branch in the repository. + +3. **Isolate the problem** — provide clear steps to reproduce. + +A good bug report shouldn't leave others needing to chase you up for more information. + +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. + +Example: + +> Short and descriptive example bug report title +> +> A summary of the issue and the OS environment in which it occurs. If +> suitable, include the steps required to reproduce the bug. +> +> 1. This is the first step +> 2. This is the second step +> 3. Further steps, etc. +> +> `` - a link to the reduced test case, if possible +> +> Any other information you want to share that is relevant to the issue being +> reported. This might include the lines of code that you have identified as +> causing the bug, and potential solutions (and your opinions on their +> merits). + +### Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. + +Do check if the feature request already exists. If it does, give it a thumbs-up emoji +or even comment. We'd like to avoid duplicate requests. + +### Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +**Please ask first** before embarking on any significant pull request (e.g. +implementing features, refactoring code, porting to a different language), +otherwise you risk spending a lot of time working on something that the +project's developers might not want to merge into the project. As far as _where_ to ask, +the feature request or bug report is the best place to go. + +Please adhere to the coding conventions used throughout a project (indentation, +accurate comments, etc.) and any other requirements (such as test coverage). + +Follow this process if you'd like your work considered for inclusion in the +project: + +1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork, + and configure the remotes: + + ```bash + # Clone your fork of the repo into the current directory + git clone git@github.com:YOUR_USERNAME/WeScan.git + # Navigate to the newly cloned directory + cd WeScan + # Assign the original repo to a remote called "upstream" + git remote add upstream git@github.com:WeTransfer/WeScan.git + ``` + +2. If you cloned a while ago, get the latest changes from upstream: + + ```bash + git checkout + git pull upstream + ``` + +3. Create a new topic branch (off the main project development branch) to + contain your feature, change, or fix: + + ```bash + git checkout -b + ``` + +4. Commit your changes in logical chunks. + +5. Locally merge (or rebase) the upstream development branch into your topic branch: + + ```bash + git pull [--rebase] upstream + ``` + +6. Push your topic branch up to your fork: + + ```bash + git push origin + ``` + +7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) + with a clear title and description. + +### Conventions of commit messages + +Adding features on repo + +```bash +git commit -m "feat: message about this feature" +``` + +Fixing features on repo + +```bash +git commit -m "fix: message about this update" +``` + +Removing features on repo + +```bash +git commit -m "refactor: message about this" -m "BREAKING CHANGE: message about the breaking change" +``` + + +**IMPORTANT**: By submitting a patch, you agree to allow the project owner to +license your work under the same license as that used by the project, which is available [here](LICENSE.md). + +### Discussions + +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. + +#### Our expectations on you as a contributor + +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. 🎉 + +The fact that you'll have push access will allow you to: + +- 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. +- Help triage issues, merge pull requests. +- Pick up the project if other maintainers move their focus elsewhere. diff --git a/ios/WeScan-3.0.0/Changelog.md b/ios/WeScan-3.0.0/Changelog.md new file mode 100644 index 0000000..3e19071 --- /dev/null +++ b/ios/WeScan-3.0.0/Changelog.md @@ -0,0 +1,80 @@ +### 3.0.0-beta.1 +- Copy snapshots to test bundle to solve SPM warning ([#363](https://github.com/WeTransfer/WeScan/pull/363)) via [@valeriyvan](https://github.com/valeriyvan) +- Fix typo ([#362](https://github.com/WeTransfer/WeScan/pull/362)) via [@valeriyvan](https://github.com/valeriyvan) +- Remove deprecated properties ([#360](https://github.com/WeTransfer/WeScan/pull/360)) via [@valeriyvan](https://github.com/valeriyvan) +- Fix typos ([#357](https://github.com/WeTransfer/WeScan/pull/357)) via [@valeriyvan](https://github.com/valeriyvan) +- Merge release 2.1.0 into master ([#352](https://github.com/WeTransfer/WeScan/pull/352)) via [@wetransferplatform](https://github.com/wetransferplatform) + +### 2.1.0 +- Update CI module to fix CI ([#351](https://github.com/WeTransfer/WeScan/pull/351)) via [@AvdLee](https://github.com/AvdLee) +- Fix resetMatchingScores ([#349](https://github.com/WeTransfer/WeScan/pull/349)) via [@lengocgiang](https://github.com/lengocgiang) +- Update CODEOWNERS ([#343](https://github.com/WeTransfer/WeScan/pull/343)) via [@peagasilva](https://github.com/peagasilva) +- Merge release 2.0.0 into master ([#342](https://github.com/WeTransfer/WeScan/pull/342)) via [@wetransferplatform](https://github.com/wetransferplatform) + +### 2.0.0 +- Fixed SwiftUI Previews in Xcode >= 14 ([#338](https://github.com/WeTransfer/WeScan/pull/338)) via [@amarildolucas](https://github.com/amarildolucas) +- Update CI to latest ([#339](https://github.com/WeTransfer/WeScan/pull/339)) via [@AvdLee](https://github.com/AvdLee) + +### 1.8.1 +- Fix broken iOS 14 AV apple api ([#293](https://github.com/WeTransfer/WeScan/pull/293)) via [@ErikGro](https://github.com/ErikGro) +- ! add japan language ([#281](https://github.com/WeTransfer/WeScan/pull/281)) via [@padgithub](https://github.com/padgithub) +- Localization - Add support for Dutch language ([#285](https://github.com/WeTransfer/WeScan/pull/285)) via [@marvukusic](https://github.com/marvukusic) +- Fix tests, update to use the iPhone 12 simulator. ([#290](https://github.com/WeTransfer/WeScan/pull/290)) via [@AvdLee](https://github.com/AvdLee) +- Merge release 1.8.0 into master ([#280](https://github.com/WeTransfer/WeScan/pull/280)) via [@wetransferplatform](https://github.com/wetransferplatform) + +### 1.8.0 +- SPM Support ([#172](https://github.com/WeTransfer/WeScan/issues/172)) via [@AvdLee](https://github.com/AvdLee) +- Typo fix in the comment ([#272](https://github.com/WeTransfer/WeScan/pull/272)) via [@PermanAtayev](https://github.com/PermanAtayev) +- Realign table of contents and rest of the README ([#271](https://github.com/WeTransfer/WeScan/pull/271)) via [@jacquerie](https://github.com/jacquerie) +- Feat: added Arabic language support ([#267](https://github.com/WeTransfer/WeScan/pull/267)) via [@mohammadhamdan1991](https://github.com/mohammadhamdan1991) +- Czech language support ([#259](https://github.com/WeTransfer/WeScan/pull/259)) via [@killalad](https://github.com/killalad) +- Merge release 1.7.0 into master ([#256](https://github.com/WeTransfer/WeScan/pull/256)) via [@ghost](https://github.com/ghost) + +### 1.7.0 +- Create individual Scanner and Review image controller ([#213](https://github.com/WeTransfer/WeScan/pull/213)) via [@chawatvish](https://github.com/chawatvish) +- Merge release 1.6.0 into master ([#254](https://github.com/WeTransfer/WeScan/pull/254)) via [@WeTransferBot](https://github.com/WeTransferBot) + +### 1.6.0 +- Allow support for using an image after instantiation ([#251](https://github.com/WeTransfer/WeScan/pull/251)) via [@erikvillegas](https://github.com/erikvillegas) +- SF Symbols ([#250](https://github.com/WeTransfer/WeScan/pull/250)) via [@andschdk](https://github.com/andschdk) +- Use same yellow tint color. ([#249](https://github.com/WeTransfer/WeScan/pull/249)) via [@andschdk](https://github.com/andschdk) +- 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) +- Fix error description typo ([#244](https://github.com/WeTransfer/WeScan/pull/244)) via [@danilovmaxim](https://github.com/danilovmaxim) +- Update EditScanViewController.swift ([#242](https://github.com/WeTransfer/WeScan/pull/242)) via [@hakan-codeway](https://github.com/hakan-codeway) +- Update Localizable.strings ([#239](https://github.com/WeTransfer/WeScan/pull/239)) via [@hakan-codeway](https://github.com/hakan-codeway) +- Update ReviewViewController.swift ([#240](https://github.com/WeTransfer/WeScan/pull/240)) via [@hakan-codeway](https://github.com/hakan-codeway) +- Safe assignment of the `AVCaptureSession` preset value. ([#238](https://github.com/WeTransfer/WeScan/pull/238)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck) +- Added delegate argument to class `CaptureSessionManager` init. ([#237](https://github.com/WeTransfer/WeScan/pull/237)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck) +- Trivial marker typo fix. ([#234](https://github.com/WeTransfer/WeScan/pull/234)) via [@davidsteppenbeck](https://github.com/davidsteppenbeck) +- [localization] add Russian language support ([#235](https://github.com/WeTransfer/WeScan/pull/235)) via [@DmitriyTor](https://github.com/DmitriyTor) +- 🇹🇷 Turkish localization added ([#231](https://github.com/WeTransfer/WeScan/pull/231)) via [@Adem68](https://github.com/Adem68) +- Re-scale height independently in Quadrilateral CGAffineTransform ([#228](https://github.com/WeTransfer/WeScan/pull/228)) via [@winsonluk](https://github.com/winsonluk) +- Merge release 1.5.0 into master ([#225](https://github.com/WeTransfer/WeScan/pull/225)) via [@WeTransferBot](https://github.com/WeTransferBot) + +### 1.5.0 +- Update xcode project - include polish translation - improvements ([#224](https://github.com/WeTransfer/WeScan/pull/224)) via @lukszar +- Create polish localization ([#221](https://github.com/WeTransfer/WeScan/pull/221)) via @lukszar +- ES and LATAM spanish translations ([#223](https://github.com/WeTransfer/WeScan/pull/223)) via @nicoonguitar +- Updated the readme to avoid some small initial configuration issues. ([#222](https://github.com/WeTransfer/WeScan/pull/222)) via @Ovid-iu +- Merge release 1.4.0 into master ([#212](https://github.com/WeTransfer/WeScan/pull/212)) via @WeTransferBot + +### 1.4.0 + +- Migrate to Bitrise & Danger-Swift ([#211](https://github.com/WeTransfer/WeScan/pull/211)) via @AvdLee + +### 1.3.0 +- Updated SwiftLint code style rules +- Forcing a changelog entry now from CI + +### 1.1.0 + +- Updated to Swift 5.0 +- Added German translation +- Several small improvements + +### 1.0 (2019-01-08) + +- Add support for French, Italian, Portuguese, Chinese (Simplified) and Chinese (Traditional) +- Add support to enhance the scanned image using AdaptiveThresholding +- Updated to Swift 4.2 +- Add auto rotate, auto scan, and Vision support \ No newline at end of file diff --git a/ios/WeScan-3.0.0/LICENSE b/ios/WeScan-3.0.0/LICENSE new file mode 100755 index 0000000..6136b0f --- /dev/null +++ b/ios/WeScan-3.0.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 WeTransfer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ios/WeScan-3.0.0/Package.resolved b/ios/WeScan-3.0.0/Package.resolved new file mode 100644 index 0000000..20c26b1 --- /dev/null +++ b/ios/WeScan-3.0.0/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "f29e2014f6230cf7d5138fc899da51c7f513d467", + "version" : "1.10.0" + } + } + ], + "version" : 2 +} diff --git a/ios/WeScan-3.0.0/Package.swift b/ios/WeScan-3.0.0/Package.swift new file mode 100644 index 0000000..bded8b9 --- /dev/null +++ b/ios/WeScan-3.0.0/Package.swift @@ -0,0 +1,35 @@ +// swift-tools-version:5.7 +// We're hiding dev, test, and danger dependencies with // dev to make sure they're not fetched by users of this package. +import PackageDescription + +let package = Package( + name: "WeScan", + defaultLocalization: "en", + platforms: [ + .iOS(.v13) + ], + products: [ + .library(name: "WeScan", targets: ["WeScan"]) + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.10.0") + ], + targets: [ + .target(name: "WeScan", + resources: [ + .process("Resources") + ]), + .testTarget( + name: "WeScanTests", + dependencies: [ + "WeScan", + .product(name: "SnapshotTesting", package: "swift-snapshot-testing") + ], + exclude:["Info.plist"], + resources: [ + .process("Resources"), + .copy("__Snapshots__") + ] + ) + ] +) diff --git a/ios/WeScan-3.0.0/README.md b/ios/WeScan-3.0.0/README.md new file mode 100644 index 0000000..02c3828 --- /dev/null +++ b/ios/WeScan-3.0.0/README.md @@ -0,0 +1,136 @@ +# WeScan + +

+ +

+ +

+ + + + + +

+ +**WeScan** makes it easy to add scanning functionalities to your iOS app! +It's modelled after `UIImagePickerController`, which makes it a breeze to use. + +- [Features](#features) +- [Demo](#demo) +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) + +## Features + +- [x] Fast and lightweight +- [x] Live scanning of documents +- [x] Edit detected rectangle +- [x] Auto scan and flash support +- [x] Support for both PDF and UIImage +- [x] Translated to English, Chinese, Italian, Portuguese, and French +- [ ] Batch scanning + +## Demo + +

+ +

+ +## Requirements + +- Swift 5.0 +- iOS 10.0+ + +
+ +## Installation + +### Swift Package Manager + +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. + +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`. + +```swift +dependencies: [ + .package(url: "https://github.com/WeTransfer/WeScan.git", .upToNextMajor(from: "2.1.0")) +] +``` + +## Usage + +### Swift + +1. In order to make the framework available, add `import WeScan` at the top of the Swift source file + +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 + +3. Make sure that your view controller conforms to the `ImageScannerControllerDelegate` protocol: + +```swift +class YourViewController: UIViewController, ImageScannerControllerDelegate { + // YourViewController code here +} +``` + +4. Implement the delegate functions inside your view controller: +```swift +func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { + // You are responsible for carefully handling the error + print(error) +} + +func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { + // The user successfully scanned an image, which is available in the ImageScannerResults + // You are responsible for dismissing the ImageScannerController + scanner.dismiss(animated: true) +} + +func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { + // The user tapped 'Cancel' on the scanner + // You are responsible for dismissing the ImageScannerController + scanner.dismiss(animated: true) +} +``` + +5. Finally, create and present a `ImageScannerController` instance somewhere within your view controller: + +```swift +let scannerViewController = ImageScannerController() +scannerViewController.imageScannerDelegate = self +present(scannerViewController, animated: true) +``` + +### Objective-C + +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' +2. In the new header, add the Objective-C class (`#import myClass.h`) where you want to use WeScan +3. In your class, import the header (`import `) +4. Drag and drop the WeScan folder to add it to your project +5. In your class, add `@Class ImageScannerController;` + +#### Example Implementation + +```objc +ImageScannerController *scannerViewController = [[ImageScannerController alloc] init]; +[self presentViewController:scannerViewController animated:YES completion:nil]; +``` + +
+ +## Contributing + +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. + +- If you **found a bug**, open an [issue](https://github.com/WeTransfer/WeScan/issues). +- If you **have a feature request**, open an [issue](https://github.com/WeTransfer/WeScan/issues). +- If you **want to contribute**, submit a [pull request](https://github.com/WeTransfer/WeScan/pulls). + +
+ +## License + +**WeScan** is available under the MIT license. See the [LICENSE](https://github.com/WeTransfer/WeScan/blob/develop/LICENSE) file for more info. diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/CIRectangleDetector.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/CIRectangleDetector.swift new file mode 100644 index 0000000..5c21bda --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/CIRectangleDetector.swift @@ -0,0 +1,41 @@ +// +// RectangleDetector.swift +// WeScan +// +// Created by Boris Emorine on 2/13/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import CoreImage +import Foundation + +/// Class used to detect rectangles from an image. +enum CIRectangleDetector { + + static let rectangleDetector = CIDetector(ofType: CIDetectorTypeRectangle, + context: CIContext(options: nil), + options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]) + + /// Detects rectangles from the given image on iOS 10. + /// + /// - Parameters: + /// - image: The image to detect rectangles on. + /// - Returns: The biggest detected rectangle on the image. + static func rectangle(forImage image: CIImage, completion: @escaping ((Quadrilateral?) -> Void)) { + let biggestRectangle = rectangle(forImage: image) + completion(biggestRectangle) + } + + static func rectangle(forImage image: CIImage) -> Quadrilateral? { + guard let rectangleFeatures = rectangleDetector?.features(in: image) as? [CIRectangleFeature] else { + return nil + } + + let quads = rectangleFeatures.map { rectangle in + return Quadrilateral(rectangleFeature: rectangle) + } + + return quads.biggest() + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/EditScanCornerView.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/EditScanCornerView.swift new file mode 100644 index 0000000..fcade9f --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/EditScanCornerView.swift @@ -0,0 +1,75 @@ +// +// EditScanCornerView.swift +// WeScan +// +// Created by Boris Emorine on 3/5/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreImage +import UIKit + +/// A UIView used by corners of a quadrilateral that is aware of its position. +final class EditScanCornerView: UIView { + + let position: CornerPosition + + /// The image to display when the corner view is highlighted. + private var image: UIImage? + private(set) var isHighlighted = false + + private lazy var circleLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.fillColor = UIColor.clear.cgColor + layer.strokeColor = UIColor.white.cgColor + layer.lineWidth = 1.0 + return layer + }() + + /// Set stroke color of corner layer + public var strokeColor: CGColor? { + didSet { + circleLayer.strokeColor = strokeColor + } + } + + init(frame: CGRect, position: CornerPosition) { + self.position = position + super.init(frame: frame) + backgroundColor = UIColor.clear + clipsToBounds = true + layer.addSublayer(circleLayer) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = bounds.width / 2.0 + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + + let bezierPath = UIBezierPath(ovalIn: rect.insetBy(dx: circleLayer.lineWidth, dy: circleLayer.lineWidth)) + circleLayer.frame = rect + circleLayer.path = bezierPath.cgPath + + image?.draw(in: rect) + } + + func highlightWithImage(_ image: UIImage) { + isHighlighted = true + self.image = image + self.setNeedsDisplay() + } + + func reset() { + isHighlighted = false + image = nil + setNeedsDisplay() + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/Error.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/Error.swift new file mode 100644 index 0000000..797e22b --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/Error.swift @@ -0,0 +1,39 @@ +// +// Error.swift +// WeScan +// +// Created by Boris Emorine on 2/28/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreImage +import Foundation + +/// Errors related to the `ImageScannerController` +public enum ImageScannerControllerError: Error { + /// The user didn't grant permission to use the camera. + case authorization + /// An error occurred when setting up the user's device. + case inputDevice + /// An error occurred when trying to capture a picture. + case capture + /// Error when creating the CIImage. + case ciImageCreation +} + +extension ImageScannerControllerError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .authorization: + return "Failed to get the user's authorization for camera." + case .inputDevice: + return "Could not setup input device." + case .capture: + return "Could not capture picture." + case .ciImageCreation: + return "Internal Error - Could not create CIImage" + } + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/Quadrilateral.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/Quadrilateral.swift new file mode 100644 index 0000000..dfa9ed7 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/Quadrilateral.swift @@ -0,0 +1,239 @@ +// +// Quadrilateral.swift +// WeScan +// +// Created by Boris Emorine on 2/8/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import CoreImage +import Foundation +import UIKit +import Vision + +/// A data structure representing a quadrilateral and its position. +/// This class exists to bypass the fact that CIRectangleFeature is read-only. +public struct Quadrilateral: Transformable { + + /// A point that specifies the top left corner of the quadrilateral. + public var topLeft: CGPoint + + /// A point that specifies the top right corner of the quadrilateral. + public var topRight: CGPoint + + /// A point that specifies the bottom right corner of the quadrilateral. + public var bottomRight: CGPoint + + /// A point that specifies the bottom left corner of the quadrilateral. + public var bottomLeft: CGPoint + + public var description: String { + return "topLeft: \(topLeft), topRight: \(topRight), bottomRight: \(bottomRight), bottomLeft: \(bottomLeft)" + } + + /// The path of the Quadrilateral as a `UIBezierPath` + var path: UIBezierPath { + let path = UIBezierPath() + path.move(to: topLeft) + path.addLine(to: topRight) + path.addLine(to: bottomRight) + path.addLine(to: bottomLeft) + path.close() + + return path + } + + /// The perimeter of the Quadrilateral + var perimeter: Double { + let perimeter = topLeft.distanceTo(point: topRight) + + topRight.distanceTo(point: bottomRight) + + bottomRight.distanceTo(point: bottomLeft) + + bottomLeft.distanceTo(point: topLeft) + return Double(perimeter) + } + + init(rectangleFeature: CIRectangleFeature) { + self.topLeft = rectangleFeature.topLeft + self.topRight = rectangleFeature.topRight + self.bottomLeft = rectangleFeature.bottomLeft + self.bottomRight = rectangleFeature.bottomRight + } + + @available(iOS 11.0, *) + init(rectangleObservation: VNRectangleObservation) { + self.topLeft = rectangleObservation.topLeft + self.topRight = rectangleObservation.topRight + self.bottomLeft = rectangleObservation.bottomLeft + self.bottomRight = rectangleObservation.bottomRight + } + + init(topLeft: CGPoint, topRight: CGPoint, bottomRight: CGPoint, bottomLeft: CGPoint) { + self.topLeft = topLeft + self.topRight = topRight + self.bottomRight = bottomRight + self.bottomLeft = bottomLeft + } + + /// Applies a `CGAffineTransform` to the quadrilateral. + /// + /// - Parameters: + /// - t: the transform to apply. + /// - Returns: The transformed quadrilateral. + func applying(_ transform: CGAffineTransform) -> Quadrilateral { + let quadrilateral = Quadrilateral( + topLeft: topLeft.applying(transform), + topRight: topRight.applying(transform), + bottomRight: bottomRight.applying(transform), + bottomLeft: bottomLeft.applying(transform) + ) + + return quadrilateral + } + + /// Checks whether the quadrilateral is within a given distance of another quadrilateral. + /// + /// - Parameters: + /// - distance: The distance (threshold) to use for the condition to be met. + /// - rectangleFeature: The other rectangle to compare this instance with. + /// - Returns: True if the given rectangle is within the given distance of this rectangle instance. + func isWithin(_ distance: CGFloat, ofRectangleFeature rectangleFeature: Quadrilateral) -> Bool { + + let topLeftRect = topLeft.surroundingSquare(withSize: distance) + if !topLeftRect.contains(rectangleFeature.topLeft) { + return false + } + + let topRightRect = topRight.surroundingSquare(withSize: distance) + if !topRightRect.contains(rectangleFeature.topRight) { + return false + } + + let bottomRightRect = bottomRight.surroundingSquare(withSize: distance) + if !bottomRightRect.contains(rectangleFeature.bottomRight) { + return false + } + + let bottomLeftRect = bottomLeft.surroundingSquare(withSize: distance) + if !bottomLeftRect.contains(rectangleFeature.bottomLeft) { + return false + } + + return true + } + + /// Reorganizes the current quadrilateral, making sure that the points are at their appropriate positions. + /// For example, it ensures that the top left point is actually the top and left point point of the quadrilateral. + mutating func reorganize() { + let points = [topLeft, topRight, bottomRight, bottomLeft] + let ySortedPoints = sortPointsByYValue(points) + + guard ySortedPoints.count == 4 else { + return + } + + let topMostPoints = Array(ySortedPoints[0..<2]) + let bottomMostPoints = Array(ySortedPoints[2..<4]) + let xSortedTopMostPoints = sortPointsByXValue(topMostPoints) + let xSortedBottomMostPoints = sortPointsByXValue(bottomMostPoints) + + guard xSortedTopMostPoints.count > 1, + xSortedBottomMostPoints.count > 1 else { + return + } + + topLeft = xSortedTopMostPoints[0] + topRight = xSortedTopMostPoints[1] + bottomRight = xSortedBottomMostPoints[1] + bottomLeft = xSortedBottomMostPoints[0] + } + + /// Scales the quadrilateral based on the ratio of two given sizes, and optionally applies a rotation. + /// + /// - Parameters: + /// - fromSize: The size the quadrilateral is currently related to. + /// - toSize: The size to scale the quadrilateral to. + /// - rotationAngle: The optional rotation to apply. + /// - Returns: The newly scaled and potentially rotated quadrilateral. + func scale(_ fromSize: CGSize, _ toSize: CGSize, withRotationAngle rotationAngle: CGFloat = 0.0) -> Quadrilateral { + var invertedFromSize = fromSize + let rotated = rotationAngle != 0.0 + + if rotated && rotationAngle != CGFloat.pi { + invertedFromSize = CGSize(width: fromSize.height, height: fromSize.width) + } + + var transformedQuad = self + let invertedFromSizeWidth = invertedFromSize.width == 0 ? .leastNormalMagnitude : invertedFromSize.width + let invertedFromSizeHeight = invertedFromSize.height == 0 ? .leastNormalMagnitude : invertedFromSize.height + + let scaleWidth = toSize.width / invertedFromSizeWidth + let scaleHeight = toSize.height / invertedFromSizeHeight + let scaledTransform = CGAffineTransform(scaleX: scaleWidth, y: scaleHeight) + transformedQuad = transformedQuad.applying(scaledTransform) + + if rotated { + let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle) + + let fromImageBounds = CGRect(origin: .zero, size: fromSize).applying(scaledTransform).applying(rotationTransform) + + let toImageBounds = CGRect(origin: .zero, size: toSize) + let translationTransform = CGAffineTransform.translateTransform( + fromCenterOfRect: fromImageBounds, + toCenterOfRect: toImageBounds + ) + + transformedQuad = transformedQuad.applyTransforms([rotationTransform, translationTransform]) + } + + return transformedQuad + } + + // Convenience functions + + /// Sorts the given `CGPoints` based on their y value. + /// - Parameters: + /// - points: The points to sort. + /// - Returns: The points sorted based on their y value. + private func sortPointsByYValue(_ points: [CGPoint]) -> [CGPoint] { + return points.sorted { point1, point2 -> Bool in + point1.y < point2.y + } + } + + /// Sorts the given `CGPoints` based on their x value. + /// - Parameters: + /// - points: The points to sort. + /// - Returns: The points sorted based on their x value. + private func sortPointsByXValue(_ points: [CGPoint]) -> [CGPoint] { + return points.sorted { point1, point2 -> Bool in + point1.x < point2.x + } + } +} + +extension Quadrilateral { + + /// Converts the current to the cartesian coordinate system (where 0 on the y axis is at the bottom). + /// + /// - Parameters: + /// - height: The height of the rect containing the quadrilateral. + /// - Returns: The same quadrilateral in the cartesian coordinate system. + func toCartesian(withHeight height: CGFloat) -> Quadrilateral { + let topLeft = self.topLeft.cartesian(withHeight: height) + let topRight = self.topRight.cartesian(withHeight: height) + let bottomRight = self.bottomRight.cartesian(withHeight: height) + let bottomLeft = self.bottomLeft.cartesian(withHeight: height) + + return Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) + } +} + +extension Quadrilateral: Equatable { + public static func == (lhs: Quadrilateral, rhs: Quadrilateral) -> Bool { + return lhs.topLeft == rhs.topLeft + && lhs.topRight == rhs.topRight + && lhs.bottomRight == rhs.bottomRight + && lhs.bottomLeft == rhs.bottomLeft + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/QuadrilateralView.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/QuadrilateralView.swift new file mode 100644 index 0000000..f44466a --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/QuadrilateralView.swift @@ -0,0 +1,314 @@ +// +// RectangleView.swift +// WeScan +// +// Created by Boris Emorine on 2/8/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import Foundation +import UIKit + +/// Simple enum to keep track of the position of the corners of a quadrilateral. +enum CornerPosition { + case topLeft + case topRight + case bottomRight + case bottomLeft +} + +/// The `QuadrilateralView` is a simple `UIView` subclass that can draw a quadrilateral, and optionally edit it. +final class QuadrilateralView: UIView { + + private let quadLayer: CAShapeLayer = { + let layer = CAShapeLayer() + layer.strokeColor = UIColor.white.cgColor + layer.lineWidth = 1.0 + layer.opacity = 1.0 + layer.isHidden = true + + return layer + }() + + /// We want the corner views to be displayed under the outline of the quadrilateral. + /// Because of that, we need the quadrilateral to be drawn on a UIView above them. + private let quadView: UIView = { + let view = UIView() + view.backgroundColor = UIColor.clear + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + /// The quadrilateral drawn on the view. + private(set) var quad: Quadrilateral? + + public var editable = false { + didSet { + cornerViews(hidden: !editable) + quadLayer.fillColor = editable ? UIColor(white: 0.0, alpha: 0.6).cgColor : UIColor(white: 1.0, alpha: 0.5).cgColor + guard let quad else { + return + } + drawQuad(quad, animated: false) + layoutCornerViews(forQuad: quad) + } + } + + /// Set stroke color of image rect and corner. + public var strokeColor: CGColor? { + didSet { + quadLayer.strokeColor = strokeColor + topLeftCornerView.strokeColor = strokeColor + topRightCornerView.strokeColor = strokeColor + bottomRightCornerView.strokeColor = strokeColor + bottomLeftCornerView.strokeColor = strokeColor + } + } + + private var isHighlighted = false { + didSet (oldValue) { + guard oldValue != isHighlighted else { + return + } + quadLayer.fillColor = isHighlighted ? UIColor.clear.cgColor : UIColor(white: 0.0, alpha: 0.6).cgColor + if isHighlighted { + bringSubviewToFront(quadView) + } else { + sendSubviewToBack(quadView) + } + } + } + + private lazy var topLeftCornerView: EditScanCornerView = { + return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .topLeft) + }() + + private lazy var topRightCornerView: EditScanCornerView = { + return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .topRight) + }() + + private lazy var bottomRightCornerView: EditScanCornerView = { + return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .bottomRight) + }() + + private lazy var bottomLeftCornerView: EditScanCornerView = { + return EditScanCornerView(frame: CGRect(origin: .zero, size: cornerViewSize), position: .bottomLeft) + }() + + private let highlightedCornerViewSize = CGSize(width: 75.0, height: 75.0) + private let cornerViewSize = CGSize(width: 20.0, height: 20.0) + + // MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func commonInit() { + addSubview(quadView) + setupCornerViews() + setupConstraints() + quadView.layer.addSublayer(quadLayer) + } + + private func setupConstraints() { + let quadViewConstraints = [ + quadView.topAnchor.constraint(equalTo: topAnchor), + quadView.leadingAnchor.constraint(equalTo: leadingAnchor), + bottomAnchor.constraint(equalTo: quadView.bottomAnchor), + trailingAnchor.constraint(equalTo: quadView.trailingAnchor) + ] + + NSLayoutConstraint.activate(quadViewConstraints) + } + + private func setupCornerViews() { + addSubview(topLeftCornerView) + addSubview(topRightCornerView) + addSubview(bottomRightCornerView) + addSubview(bottomLeftCornerView) + } + + override public func layoutSubviews() { + super.layoutSubviews() + guard quadLayer.frame != bounds else { + return + } + + quadLayer.frame = bounds + if let quad { + drawQuadrilateral(quad: quad, animated: false) + } + } + + // MARK: - Drawings + + /// Draws the passed in quadrilateral. + /// + /// - Parameters: + /// - quad: The quadrilateral to draw on the view. It should be in the coordinates of the current `QuadrilateralView` instance. + func drawQuadrilateral(quad: Quadrilateral, animated: Bool) { + self.quad = quad + drawQuad(quad, animated: animated) + if editable { + cornerViews(hidden: false) + layoutCornerViews(forQuad: quad) + } + } + + private func drawQuad(_ quad: Quadrilateral, animated: Bool) { + var path = quad.path + + if editable { + path = path.reversing() + let rectPath = UIBezierPath(rect: bounds) + path.append(rectPath) + } + + if animated == true { + let pathAnimation = CABasicAnimation(keyPath: "path") + pathAnimation.duration = 0.2 + quadLayer.add(pathAnimation, forKey: "path") + } + + quadLayer.path = path.cgPath + quadLayer.isHidden = false + } + + private func layoutCornerViews(forQuad quad: Quadrilateral) { + topLeftCornerView.center = quad.topLeft + topRightCornerView.center = quad.topRight + bottomLeftCornerView.center = quad.bottomLeft + bottomRightCornerView.center = quad.bottomRight + } + + func removeQuadrilateral() { + quadLayer.path = nil + quadLayer.isHidden = true + } + + // MARK: - Actions + + func moveCorner(cornerView: EditScanCornerView, atPoint point: CGPoint) { + guard let quad else { + return + } + + let validPoint = self.validPoint(point, forCornerViewOfSize: cornerView.bounds.size, inView: self) + + cornerView.center = validPoint + let updatedQuad = update(quad, withPosition: validPoint, forCorner: cornerView.position) + + self.quad = updatedQuad + drawQuad(updatedQuad, animated: false) + } + + func highlightCornerAtPosition(position: CornerPosition, with image: UIImage) { + guard editable else { + return + } + isHighlighted = true + + let cornerView = cornerViewForCornerPosition(position: position) + guard cornerView.isHighlighted == false else { + cornerView.highlightWithImage(image) + return + } + + let origin = CGPoint(x: cornerView.frame.origin.x - (highlightedCornerViewSize.width - cornerViewSize.width) / 2.0, + y: cornerView.frame.origin.y - (highlightedCornerViewSize.height - cornerViewSize.height) / 2.0) + cornerView.frame = CGRect(origin: origin, size: highlightedCornerViewSize) + cornerView.highlightWithImage(image) + } + + func resetHighlightedCornerViews() { + isHighlighted = false + resetHighlightedCornerViews(cornerViews: [topLeftCornerView, topRightCornerView, bottomLeftCornerView, bottomRightCornerView]) + } + + private func resetHighlightedCornerViews(cornerViews: [EditScanCornerView]) { + cornerViews.forEach { cornerView in + resetHighlightedCornerView(cornerView: cornerView) + } + } + + private func resetHighlightedCornerView(cornerView: EditScanCornerView) { + cornerView.reset() + let origin = CGPoint(x: cornerView.frame.origin.x + (cornerView.frame.size.width - cornerViewSize.width) / 2.0, + y: cornerView.frame.origin.y + (cornerView.frame.size.height - cornerViewSize.width) / 2.0) + cornerView.frame = CGRect(origin: origin, size: cornerViewSize) + cornerView.setNeedsDisplay() + } + + // MARK: Validation + + /// Ensures that the given point is valid - meaning that it is within the bounds of the passed in `UIView`. + /// + /// - Parameters: + /// - point: The point that needs to be validated. + /// - cornerViewSize: The size of the corner view representing the given point. + /// - view: The view which should include the point. + /// - Returns: A new point which is within the passed in view. + private func validPoint(_ point: CGPoint, forCornerViewOfSize cornerViewSize: CGSize, inView view: UIView) -> CGPoint { + var validPoint = point + + if point.x > view.bounds.width { + validPoint.x = view.bounds.width + } else if point.x < 0.0 { + validPoint.x = 0.0 + } + + if point.y > view.bounds.height { + validPoint.y = view.bounds.height + } else if point.y < 0.0 { + validPoint.y = 0.0 + } + + return validPoint + } + + // MARK: - Convenience + + private func cornerViews(hidden: Bool) { + topLeftCornerView.isHidden = hidden + topRightCornerView.isHidden = hidden + bottomRightCornerView.isHidden = hidden + bottomLeftCornerView.isHidden = hidden + } + + private func update(_ quad: Quadrilateral, withPosition position: CGPoint, forCorner corner: CornerPosition) -> Quadrilateral { + var quad = quad + + switch corner { + case .topLeft: + quad.topLeft = position + case .topRight: + quad.topRight = position + case .bottomRight: + quad.bottomRight = position + case .bottomLeft: + quad.bottomLeft = position + } + + return quad + } + + func cornerViewForCornerPosition(position: CornerPosition) -> EditScanCornerView { + switch position { + case .topLeft: + return topLeftCornerView + case .topRight: + return topRightCornerView + case .bottomLeft: + return bottomLeftCornerView + case .bottomRight: + return bottomRightCornerView + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Common/VisionRectangleDetector.swift b/ios/WeScan-3.0.0/Sources/WeScan/Common/VisionRectangleDetector.swift new file mode 100644 index 0000000..9cf57f1 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Common/VisionRectangleDetector.swift @@ -0,0 +1,99 @@ +// +// VisionRectangleDetector.swift +// WeScan +// +// Created by Julian Schiavo on 28/7/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreImage +import Foundation +import Vision + +/// Enum encapsulating static functions to detect rectangles from an image. +@available(iOS 11.0, *) +enum VisionRectangleDetector { + + private static func completeImageRequest( + for request: VNImageRequestHandler, + width: CGFloat, + height: CGFloat, + completion: @escaping ((Quadrilateral?) -> Void) + ) { + // Create the rectangle request, and, if found, return the biggest rectangle (else return nothing). + let rectangleDetectionRequest: VNDetectRectanglesRequest = { + let rectDetectRequest = VNDetectRectanglesRequest(completionHandler: { request, error in + guard error == nil, let results = request.results as? [VNRectangleObservation], !results.isEmpty else { + completion(nil) + return + } + + let quads: [Quadrilateral] = results.map(Quadrilateral.init) + + // This can't fail because the earlier guard protected against an empty array, but we use guard because of SwiftLint + guard let biggest = quads.biggest() else { + completion(nil) + return + } + + let transform = CGAffineTransform.identity + .scaledBy(x: width, y: height) + + completion(biggest.applying(transform)) + }) + + rectDetectRequest.minimumConfidence = 0.8 + rectDetectRequest.maximumObservations = 15 + rectDetectRequest.minimumAspectRatio = 0.3 + + return rectDetectRequest + }() + + // Send the requests to the request handler. + do { + try request.perform([rectangleDetectionRequest]) + } catch { + completion(nil) + return + } + + } + + /// Detects rectangles from the given CVPixelBuffer/CVImageBuffer on iOS 11 and above. + /// + /// - Parameters: + /// - pixelBuffer: The pixelBuffer to detect rectangles on. + /// - completion: The biggest rectangle on the CVPixelBuffer + static func rectangle(forPixelBuffer pixelBuffer: CVPixelBuffer, completion: @escaping ((Quadrilateral?) -> Void)) { + let imageRequestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:]) + VisionRectangleDetector.completeImageRequest( + for: imageRequestHandler, + width: CGFloat(CVPixelBufferGetWidth(pixelBuffer)), + height: CGFloat(CVPixelBufferGetHeight(pixelBuffer)), + completion: completion) + } + + /// Detects rectangles from the given image on iOS 11 and above. + /// + /// - Parameters: + /// - image: The image to detect rectangles on. + /// - Returns: The biggest rectangle detected on the image. + static func rectangle(forImage image: CIImage, completion: @escaping ((Quadrilateral?) -> Void)) { + let imageRequestHandler = VNImageRequestHandler(ciImage: image, options: [:]) + VisionRectangleDetector.completeImageRequest( + for: imageRequestHandler, width: image.extent.width, + height: image.extent.height, completion: completion) + } + + static func rectangle( + forImage image: CIImage, + orientation: CGImagePropertyOrientation, + completion: @escaping ((Quadrilateral?) -> Void) + ) { + let imageRequestHandler = VNImageRequestHandler(ciImage: image, orientation: orientation, options: [:]) + let orientedImage = image.oriented(orientation) + VisionRectangleDetector.completeImageRequest( + for: imageRequestHandler, width: orientedImage.extent.width, + height: orientedImage.extent.height, completion: completion) + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditImageViewController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditImageViewController.swift new file mode 100644 index 0000000..f29aed0 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditImageViewController.swift @@ -0,0 +1,205 @@ +// +// EditImageViewController.swift +// WeScan +// +// Created by Chawatvish Worrapoj on 7/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import AVFoundation +import UIKit + +/// A protocol that your delegate object will get results of EditImageViewController. +public protocol EditImageViewDelegate: AnyObject { + /// A method that your delegate object must implement to get cropped image. + func cropped(image: UIImage) +} + +/// A view controller that manages edit image for scanning documents or pick image from photo library +/// The `EditImageViewController` class is individual for rotate, crop image +public final class EditImageViewController: UIViewController { + + /// The image the quadrilateral was detected on. + private var image: UIImage + + /// The detected quadrilateral that can be edited by the user. Uses the image's coordinates. + private var quad: Quadrilateral + private var zoomGestureController: ZoomGestureController! + private var quadViewWidthConstraint = NSLayoutConstraint() + private var quadViewHeightConstraint = NSLayoutConstraint() + public weak var delegate: EditImageViewDelegate? + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.isOpaque = true + imageView.image = image + imageView.backgroundColor = .black + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var quadView: QuadrilateralView = { + let quadView = QuadrilateralView() + quadView.editable = true + quadView.strokeColor = strokeColor + quadView.translatesAutoresizingMaskIntoConstraints = false + return quadView + }() + + private var strokeColor: CGColor? + + // MARK: - Life Cycle + + public init(image: UIImage, quad: Quadrilateral?, rotateImage: Bool = true, strokeColor: CGColor? = nil) { + self.image = rotateImage ? image.applyingPortraitOrientation() : image + self.quad = quad ?? EditImageViewController.defaultQuad(allOfImage: image) + self.strokeColor = strokeColor + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + setupViews() + setupConstraints() + zoomGestureController = ZoomGestureController(image: image, quadView: quadView) + addLongGesture(of: zoomGestureController) + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + adjustQuadViewConstraints() + displayQuad() + } + + // MARK: - Setups + + private func setupViews() { + view.addSubview(imageView) + view.addSubview(quadView) + } + + private func setupConstraints() { + let imageViewConstraints = [ + imageView.topAnchor.constraint(equalTo: view.topAnchor), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor) + ] + + quadViewWidthConstraint = quadView.widthAnchor.constraint(equalToConstant: 0.0) + quadViewHeightConstraint = quadView.heightAnchor.constraint(equalToConstant: 0.0) + + let quadViewConstraints = [ + quadView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + quadView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + quadViewWidthConstraint, + quadViewHeightConstraint + ] + + NSLayoutConstraint.activate(quadViewConstraints + imageViewConstraints) + } + + private func addLongGesture(of controller: ZoomGestureController) { + let touchDown = UILongPressGestureRecognizer(target: controller, + action: #selector(controller.handle(pan:))) + touchDown.minimumPressDuration = 0 + view.addGestureRecognizer(touchDown) + } + + // MARK: - Actions + /// This function allow user can crop image follow quad. the image will send back by delegate function + public func cropImage() { + guard let quad = quadView.quad, let ciImage = CIImage(image: image) else { + return + } + + let cgOrientation = CGImagePropertyOrientation(image.imageOrientation) + let orientedImage = ciImage.oriented(forExifOrientation: Int32(cgOrientation.rawValue)) + let scaledQuad = quad.scale(quadView.bounds.size, image.size) + self.quad = scaledQuad + + // Cropped Image + var cartesianScaledQuad = scaledQuad.toCartesian(withHeight: image.size.height) + cartesianScaledQuad.reorganize() + + let filteredImage = orientedImage.applyingFilter("CIPerspectiveCorrection", parameters: [ + "inputTopLeft": CIVector(cgPoint: cartesianScaledQuad.bottomLeft), + "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight), + "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft), + "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight) + ]) + + let croppedImage = UIImage.from(ciImage: filteredImage) + delegate?.cropped(image: croppedImage) + } + + /// This function allow user to rotate image by 90 degree each and will reload image on image view. + public func rotateImage() { + let rotationAngle = Measurement(value: 90, unit: .degrees) + reloadImage(withAngle: rotationAngle) + } + + private func reloadImage(withAngle angle: Measurement) { + guard let newImage = image.rotated(by: angle) else { return } + let newQuad = EditImageViewController.defaultQuad(allOfImage: newImage) + + image = newImage + imageView.image = image + quad = newQuad + adjustQuadViewConstraints() + displayQuad() + + zoomGestureController = ZoomGestureController(image: image, quadView: quadView) + addLongGesture(of: zoomGestureController) + } + + private func displayQuad() { + let imageSize = image.size + let size = CGSize(width: quadViewWidthConstraint.constant, height: quadViewHeightConstraint.constant) + let imageFrame = CGRect(origin: quadView.frame.origin, size: size) + + let scaleTransform = CGAffineTransform.scaleTransform(forSize: imageSize, aspectFillInSize: imageFrame.size) + let transforms = [scaleTransform] + let transformedQuad = quad.applyTransforms(transforms) + + quadView.drawQuadrilateral(quad: transformedQuad, animated: false) + } + + /// The quadView should be lined up on top of the actual image displayed by the imageView. + /// Since there is no way to know the size of that image before run time, we adjust the constraints + /// to make sure that the quadView is on top of the displayed image. + private func adjustQuadViewConstraints() { + let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds) + quadViewWidthConstraint.constant = frame.size.width + quadViewHeightConstraint.constant = frame.size.height + } + + /// Generates a `Quadrilateral` object that's centered and one third of the size of the passed in image. + private static func defaultQuad(forImage image: UIImage) -> Quadrilateral { + let topLeft = CGPoint(x: image.size.width / 3.0, y: image.size.height / 3.0) + let topRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: image.size.height / 3.0) + let bottomRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) + let bottomLeft = CGPoint(x: image.size.width / 3.0, y: 2.0 * image.size.height / 3.0) + + let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) + + return quad + } + + /// Generates a `Quadrilateral` object that's cover all of image. + private static func defaultQuad(allOfImage image: UIImage, withOffset offset: CGFloat = 75) -> Quadrilateral { + let topLeft = CGPoint(x: offset, y: offset) + let topRight = CGPoint(x: image.size.width - offset, y: offset) + let bottomRight = CGPoint(x: image.size.width - offset, y: image.size.height - offset) + let bottomLeft = CGPoint(x: offset, y: image.size.height - offset) + let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) + return quad + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditScanViewController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditScanViewController.swift new file mode 100644 index 0000000..344bd69 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Edit/EditScanViewController.swift @@ -0,0 +1,230 @@ +// +// EditScanViewController.swift +// WeScan +// +// Created by Boris Emorine on 2/12/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import UIKit + +/// The `EditScanViewController` offers an interface for the user to edit the detected quadrilateral. +final class EditScanViewController: UIViewController { + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.isOpaque = true + imageView.image = image + imageView.backgroundColor = .black + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var quadView: QuadrilateralView = { + let quadView = QuadrilateralView() + quadView.editable = true + quadView.translatesAutoresizingMaskIntoConstraints = false + return quadView + }() + + private lazy var nextButton: UIBarButtonItem = { + let title = NSLocalizedString("wescan.edit.button.next", + tableName: nil, + bundle: Bundle(for: EditScanViewController.self), + value: "Next", + comment: "A generic next button" + ) + let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(pushReviewController)) + button.tintColor = navigationController?.navigationBar.tintColor + return button + }() + + private lazy var cancelButton: UIBarButtonItem = { + let title = NSLocalizedString("wescan.scanning.cancel", + tableName: nil, + bundle: Bundle(for: EditScanViewController.self), + value: "Cancel", + comment: "A generic cancel button" + ) + let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(cancelButtonTapped)) + button.tintColor = navigationController?.navigationBar.tintColor + return button + }() + + /// The image the quadrilateral was detected on. + private let image: UIImage + + /// The detected quadrilateral that can be edited by the user. Uses the image's coordinates. + private var quad: Quadrilateral + + private var zoomGestureController: ZoomGestureController! + + private var quadViewWidthConstraint = NSLayoutConstraint() + private var quadViewHeightConstraint = NSLayoutConstraint() + + // MARK: - Life Cycle + + init(image: UIImage, quad: Quadrilateral?, rotateImage: Bool = true) { + self.image = rotateImage ? image.applyingPortraitOrientation() : image + self.quad = quad ?? EditScanViewController.defaultQuad(forImage: image) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + + setupViews() + setupConstraints() + title = NSLocalizedString("wescan.edit.title", + tableName: nil, + bundle: Bundle(for: EditScanViewController.self), + value: "Edit Scan", + comment: "The title of the EditScanViewController" + ) + navigationItem.rightBarButtonItem = nextButton + if let firstVC = self.navigationController?.viewControllers.first, firstVC == self { + navigationItem.leftBarButtonItem = cancelButton + } else { + navigationItem.leftBarButtonItem = nil + } + + zoomGestureController = ZoomGestureController(image: image, quadView: quadView) + + let touchDown = UILongPressGestureRecognizer(target: zoomGestureController, action: #selector(zoomGestureController.handle(pan:))) + touchDown.minimumPressDuration = 0 + view.addGestureRecognizer(touchDown) + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + adjustQuadViewConstraints() + displayQuad() + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // Work around for an iOS 11.2 bug where UIBarButtonItems don't get back to their normal state after being pressed. + navigationController?.navigationBar.tintAdjustmentMode = .normal + navigationController?.navigationBar.tintAdjustmentMode = .automatic + } + + // MARK: - Setups + + private func setupViews() { + view.addSubview(imageView) + view.addSubview(quadView) + } + + private func setupConstraints() { + let imageViewConstraints = [ + imageView.topAnchor.constraint(equalTo: view.topAnchor), + imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor) + ] + + quadViewWidthConstraint = quadView.widthAnchor.constraint(equalToConstant: 0.0) + quadViewHeightConstraint = quadView.heightAnchor.constraint(equalToConstant: 0.0) + + let quadViewConstraints = [ + quadView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + quadView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + quadViewWidthConstraint, + quadViewHeightConstraint + ] + + NSLayoutConstraint.activate(quadViewConstraints + imageViewConstraints) + } + + // MARK: - Actions + @objc func cancelButtonTapped() { + if let imageScannerController = navigationController as? ImageScannerController { + imageScannerController.imageScannerDelegate?.imageScannerControllerDidCancel(imageScannerController) + } + } + + @objc func pushReviewController() { + guard let quad = quadView.quad, + let ciImage = CIImage(image: image) else { + if let imageScannerController = navigationController as? ImageScannerController { + let error = ImageScannerControllerError.ciImageCreation + imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFailWithError: error) + } + return + } + let cgOrientation = CGImagePropertyOrientation(image.imageOrientation) + let orientedImage = ciImage.oriented(forExifOrientation: Int32(cgOrientation.rawValue)) + let scaledQuad = quad.scale(quadView.bounds.size, image.size) + self.quad = scaledQuad + + // Cropped Image + var cartesianScaledQuad = scaledQuad.toCartesian(withHeight: image.size.height) + cartesianScaledQuad.reorganize() + + let filteredImage = orientedImage.applyingFilter("CIPerspectiveCorrection", parameters: [ + "inputTopLeft": CIVector(cgPoint: cartesianScaledQuad.bottomLeft), + "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight), + "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft), + "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight) + ]) + + let croppedImage = UIImage.from(ciImage: filteredImage) + // Enhanced Image + let enhancedImage = filteredImage.applyingAdaptiveThreshold()?.withFixedOrientation() + let enhancedScan = enhancedImage.flatMap { ImageScannerScan(image: $0) } + + let results = ImageScannerResults( + detectedRectangle: scaledQuad, + originalScan: ImageScannerScan(image: image), + croppedScan: ImageScannerScan(image: croppedImage), + enhancedScan: enhancedScan + ) + + let reviewViewController = ReviewViewController(results: results) + navigationController?.pushViewController(reviewViewController, animated: true) + } + + private func displayQuad() { + let imageSize = image.size + let imageFrame = CGRect( + origin: quadView.frame.origin, + size: CGSize(width: quadViewWidthConstraint.constant, height: quadViewHeightConstraint.constant) + ) + + let scaleTransform = CGAffineTransform.scaleTransform(forSize: imageSize, aspectFillInSize: imageFrame.size) + let transforms = [scaleTransform] + let transformedQuad = quad.applyTransforms(transforms) + + quadView.drawQuadrilateral(quad: transformedQuad, animated: false) + } + + /// The quadView should be lined up on top of the actual image displayed by the imageView. + /// Since there is no way to know the size of that image before run time, we adjust the constraints + /// to make sure that the quadView is on top of the displayed image. + private func adjustQuadViewConstraints() { + let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds) + quadViewWidthConstraint.constant = frame.size.width + quadViewHeightConstraint.constant = frame.size.height + } + + /// Generates a `Quadrilateral` object that's centered and 90% of the size of the passed in image. + private static func defaultQuad(forImage image: UIImage) -> Quadrilateral { + let topLeft = CGPoint(x: image.size.width * 0.05, y: image.size.height * 0.05) + let topRight = CGPoint(x: image.size.width * 0.95, y: image.size.height * 0.05) + let bottomRight = CGPoint(x: image.size.width * 0.95, y: image.size.height * 0.95) + let bottomLeft = CGPoint(x: image.size.width * 0.05, y: image.size.height * 0.95) + + let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft) + + return quad + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Edit/ZoomGestureController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Edit/ZoomGestureController.swift new file mode 100644 index 0000000..65aa1b1 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Edit/ZoomGestureController.swift @@ -0,0 +1,64 @@ +// +// ZoomGestureController.swift +// WeScan +// +// Created by Bobo on 5/31/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import Foundation +import UIKit + +final class ZoomGestureController { + + private let image: UIImage + private let quadView: QuadrilateralView + private var previousPanPosition: CGPoint? + private var closestCorner: CornerPosition? + + init(image: UIImage, quadView: QuadrilateralView) { + self.image = image + self.quadView = quadView + } + + @objc func handle(pan: UIGestureRecognizer) { + guard let drawnQuad = quadView.quad else { + return + } + + guard pan.state != .ended else { + self.previousPanPosition = nil + self.closestCorner = nil + quadView.resetHighlightedCornerViews() + return + } + + let position = pan.location(in: quadView) + + let previousPanPosition = self.previousPanPosition ?? position + let closestCorner = self.closestCorner ?? position.closestCornerFrom(quad: drawnQuad) + + let offset = CGAffineTransform(translationX: position.x - previousPanPosition.x, y: position.y - previousPanPosition.y) + let cornerView = quadView.cornerViewForCornerPosition(position: closestCorner) + let draggedCornerViewCenter = cornerView.center.applying(offset) + + quadView.moveCorner(cornerView: cornerView, atPoint: draggedCornerViewCenter) + + self.previousPanPosition = position + self.closestCorner = closestCorner + + let scale = image.size.width / quadView.bounds.size.width + let scaledDraggedCornerViewCenter = CGPoint(x: draggedCornerViewCenter.x * scale, y: draggedCornerViewCenter.y * scale) + guard let zoomedImage = image.scaledImage( + atPoint: scaledDraggedCornerViewCenter, + scaleFactor: 2.5, + targetSize: quadView.bounds.size + ) else { + return + } + + quadView.highlightCornerAtPosition(position: closestCorner, with: zoomedImage) + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift new file mode 100644 index 0000000..c977ab0 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/AVCaptureVideoOrientation+Utils.swift @@ -0,0 +1,36 @@ +// +// UIDeviceOrientation+Utils.swift +// WeScan +// +// Created by Boris Emorine on 2/13/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import CoreImage +import Foundation +import UIKit + +extension AVCaptureVideoOrientation { + + /// Maps UIDeviceOrientation to AVCaptureVideoOrientation + init?(deviceOrientation: UIDeviceOrientation) { + switch deviceOrientation { + case .portrait: + self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) + case .portraitUpsideDown: + self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue) + case .landscapeLeft: + self.init(rawValue: AVCaptureVideoOrientation.landscapeLeft.rawValue) + case .landscapeRight: + self.init(rawValue: AVCaptureVideoOrientation.landscapeRight.rawValue) + case .faceUp: + self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) + case .faceDown: + self.init(rawValue: AVCaptureVideoOrientation.portraitUpsideDown.rawValue) + default: + self.init(rawValue: AVCaptureVideoOrientation.portrait.rawValue) + } + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/Array+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/Array+Utils.swift new file mode 100644 index 0000000..0c87f46 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/Array+Utils.swift @@ -0,0 +1,23 @@ +// +// Array+Utils.swift +// WeScan +// +// Created by Boris Emorine on 2/8/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import Vision + +extension Array where Element == Quadrilateral { + + /// Finds the biggest rectangle within an array of `Quadrilateral` objects. + func biggest() -> Quadrilateral? { + let biggestRectangle = self.max(by: { rect1, rect2 -> Bool in + return rect1.perimeter < rect2.perimeter + }) + + return biggestRectangle + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGAffineTransform+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGAffineTransform+Utils.swift new file mode 100644 index 0000000..e696d86 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGAffineTransform+Utils.swift @@ -0,0 +1,37 @@ +// +// CGAffineTransform+Utils.swift +// WeScan +// +// Created by Boris Emorine on 2/15/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreImage +import Foundation +import UIKit + +extension CGAffineTransform { + + /// Convenience function to easily get a scale `CGAffineTransform` instance. + /// + /// - Parameters: + /// - fromSize: The size that needs to be transformed to fit (aspect fill) in the other given size. + /// - toSize: The size that should be matched by the `fromSize` parameter. + /// - Returns: The transform that will make the `fromSize` parameter fir (aspect fill) inside the `toSize` parameter. + static func scaleTransform(forSize fromSize: CGSize, aspectFillInSize toSize: CGSize) -> CGAffineTransform { + let scale = max(toSize.width / fromSize.width, toSize.height / fromSize.height) + return CGAffineTransform(scaleX: scale, y: scale) + } + + /// Convenience function to easily get a translate `CGAffineTransform` instance. + /// + /// - Parameters: + /// - fromRect: The rect which center needs to be translated to the center of the other passed in rect. + /// - toRect: The rect that should be matched. + /// - Returns: The transform that will translate the center of the `fromRect` parameter to the center of the `toRect` parameter. + static func translateTransform(fromCenterOfRect fromRect: CGRect, toCenterOfRect toRect: CGRect) -> CGAffineTransform { + let translate = CGPoint(x: toRect.midX - fromRect.midX, y: toRect.midY - fromRect.midY) + return CGAffineTransform(translationX: translate.x, y: translate.y) + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGImagePropertyOrientation.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGImagePropertyOrientation.swift new file mode 100644 index 0000000..64a1c7f --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGImagePropertyOrientation.swift @@ -0,0 +1,36 @@ +// +// CGImagePropertyOrientation.swift +// WeScan +// +// Created by Yang Chen on 5/21/19. +// Copyright © 2019 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension CGImagePropertyOrientation { + init(_ uiOrientation: UIImage.Orientation) { + switch uiOrientation { + case .up: + self = .up + case .upMirrored: + self = .upMirrored + case .down: + self = .down + case .downMirrored: + self = .downMirrored + case .left: + self = .left + case .leftMirrored: + self = .leftMirrored + case .right: + self = .right + case .rightMirrored: + self = .rightMirrored + @unknown default: + assertionFailure("Unknown orientation, falling to default") + self = .right + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGPoint+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGPoint+Utils.swift new file mode 100644 index 0000000..4176a18 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGPoint+Utils.swift @@ -0,0 +1,70 @@ +// +// CGPoint+Utils.swift +// WeScan +// +// Created by Boris Emorine on 2/9/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension CGPoint { + + /// Returns a rectangle of a given size surrounding the point. + /// + /// - Parameters: + /// - size: The size of the rectangle that should surround the points. + /// - Returns: A `CGRect` instance that surrounds this instance of `CGPoint`. + func surroundingSquare(withSize size: CGFloat) -> CGRect { + return CGRect(x: x - size / 2.0, y: y - size / 2.0, width: size, height: size) + } + + /// Checks wether this point is within a given distance of another point. + /// + /// - Parameters: + /// - delta: The minimum distance to meet for this distance to return true. + /// - point: The second point to compare this instance with. + /// - Returns: True if the given `CGPoint` is within the given distance of this instance of `CGPoint`. + func isWithin(delta: CGFloat, ofPoint point: CGPoint) -> Bool { + return (abs(x - point.x) <= delta) && (abs(y - point.y) <= delta) + } + + /// Returns the same `CGPoint` in the cartesian coordinate system. + /// + /// - Parameters: + /// - height: The height of the bounds this points belong to, in the current coordinate system. + /// - Returns: The same point in the cartesian coordinate system. + func cartesian(withHeight height: CGFloat) -> CGPoint { + return CGPoint(x: x, y: height - y) + } + + /// Returns the distance between two points + func distanceTo(point: CGPoint) -> CGFloat { + return hypot((self.x - point.x), (self.y - point.y)) + } + + /// Returns the closest corner from the point + func closestCornerFrom(quad: Quadrilateral) -> CornerPosition { + var smallestDistance = distanceTo(point: quad.topLeft) + var closestCorner = CornerPosition.topLeft + + if distanceTo(point: quad.topRight) < smallestDistance { + smallestDistance = distanceTo(point: quad.topRight) + closestCorner = .topRight + } + + if distanceTo(point: quad.bottomRight) < smallestDistance { + smallestDistance = distanceTo(point: quad.bottomRight) + closestCorner = .bottomRight + } + + if distanceTo(point: quad.bottomLeft) < smallestDistance { + smallestDistance = distanceTo(point: quad.bottomLeft) + closestCorner = .bottomLeft + } + + return closestCorner + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGRect+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGRect+Utils.swift new file mode 100644 index 0000000..86d8880 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGRect+Utils.swift @@ -0,0 +1,31 @@ +// +// CGRect+Utils.swift +// WeScan +// +// Created by Boris Emorine on 2/26/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension CGRect { + + /// Returns a new `CGRect` instance scaled up or down, with the same center as the original `CGRect` instance. + /// - Parameters: + /// - ratio: The ratio to scale the `CGRect` instance by. + /// - Returns: A new instance of `CGRect` scaled by the given ratio and centered with the original rect. + func scaleAndCenter(withRatio ratio: CGFloat) -> CGRect { + let scaleTransform = CGAffineTransform(scaleX: ratio, y: ratio) + let scaledRect = applying(scaleTransform) + + let translateTransform = CGAffineTransform( + translationX: origin.x * (1 - ratio) + (width - scaledRect.width) / 2.0, + y: origin.y * (1 - ratio) + (height - scaledRect.height) / 2.0 + ) + let translatedRect = scaledRect.applying(translateTransform) + + return translatedRect + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGSize+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGSize+Utils.swift new file mode 100644 index 0000000..b4ebb53 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CGSize+Utils.swift @@ -0,0 +1,27 @@ +// +// CGSize+Utils.swift +// WeScan +// +// Created by Julian Schiavo on 17/2/2019. +// Copyright © 2019 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension CGSize { + /// Calculates an appropriate scale factor which makes the size fit inside both the `maxWidth` and `maxHeight`. + /// - Parameters: + /// - maxWidth: The maximum width that the size should have after applying the scale factor. + /// - maxHeight: The maximum height that the size should have after applying the scale factor. + /// - Returns: A scale factor that makes the size fit within the `maxWidth` and `maxHeight`. + func scaleFactor(forMaxWidth maxWidth: CGFloat, maxHeight: CGFloat) -> CGFloat { + if width < maxWidth && height < maxHeight { return 1 } + + let widthScaleFactor = 1 / (width / maxWidth) + let heightScaleFactor = 1 / (height / maxHeight) + + // Use the smaller scale factor to ensure both the width and height are below the max + return min(widthScaleFactor, heightScaleFactor) + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CIImage+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CIImage+Utils.swift new file mode 100644 index 0000000..c6c911d --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/CIImage+Utils.swift @@ -0,0 +1,40 @@ +// +// CIImage+Utils.swift +// WeScan +// +// Created by Julian Schiavo on 14/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreImage +import Foundation +import UIKit + +extension CIImage { + /// Applies an AdaptiveThresholding filter to the image, which enhances the image and makes it completely gray scale + func applyingAdaptiveThreshold() -> UIImage? { + guard let colorKernel = CIColorKernel(source: + """ + kernel vec4 color(__sample pixel, float inputEdgeO, float inputEdge1) + { + float luma = dot(pixel.rgb, vec3(0.2126, 0.7152, 0.0722)); + float threshold = smoothstep(inputEdgeO, inputEdge1, luma); + return vec4(threshold, threshold, threshold, 1.0); + } + """ + ) else { return nil } + + let firstInputEdge = 0.25 + let secondInputEdge = 0.75 + + let arguments: [Any] = [self, firstInputEdge, secondInputEdge] + + guard let enhancedCIImage = colorKernel.apply(extent: self.extent, arguments: arguments) else { return nil } + + if let cgImage = CIContext(options: nil).createCGImage(enhancedCIImage, from: enhancedCIImage.extent) { + return UIImage(cgImage: cgImage) + } else { + return UIImage(ciImage: enhancedCIImage, scale: 1.0, orientation: .up) + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Orientation.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Orientation.swift new file mode 100644 index 0000000..5b5154d --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Orientation.swift @@ -0,0 +1,99 @@ +// +// UIImage+Orientation.swift +// WeScan +// +// Created by Boris Emorine on 2/16/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension UIImage { + + /// Data structure to easily express rotation options. + struct RotationOptions: OptionSet { + let rawValue: Int + + static let flipOnVerticalAxis = RotationOptions(rawValue: 1) + static let flipOnHorizontalAxis = RotationOptions(rawValue: 2) + } + + /// Returns the same image with a portrait orientation. + func applyingPortraitOrientation() -> UIImage { + switch imageOrientation { + case .up: + return rotated(by: Measurement(value: Double.pi, unit: .radians), options: []) ?? self + case .down: + return rotated(by: Measurement(value: Double.pi, unit: .radians), options: [.flipOnVerticalAxis, .flipOnHorizontalAxis]) ?? self + case .left: + return self + case .right: + return rotated(by: Measurement(value: Double.pi / 2.0, unit: .radians), options: []) ?? self + default: + return self + } + } + + /// Rotate the image by the given angle, and perform other transformations based on the passed in options. + /// + /// - Parameters: + /// - rotationAngle: The angle to rotate the image by. + /// - options: Options to apply to the image. + /// - Returns: The new image rotated and optionally flipped (@see options). + func rotated(by rotationAngle: Measurement, options: RotationOptions = []) -> UIImage? { + guard let cgImage = self.cgImage else { return nil } + + let rotationInRadians = CGFloat(rotationAngle.converted(to: .radians).value) + let transform = CGAffineTransform(rotationAngle: rotationInRadians) + let cgImageSize = CGSize(width: cgImage.width, height: cgImage.height) + var rect = CGRect(origin: .zero, size: cgImageSize).applying(transform) + rect.origin = .zero + + let format = UIGraphicsImageRendererFormat() + format.scale = 1 + + let renderer = UIGraphicsImageRenderer(size: rect.size, format: format) + + let image = renderer.image { renderContext in + renderContext.cgContext.translateBy(x: rect.midX, y: rect.midY) + renderContext.cgContext.rotate(by: rotationInRadians) + + let x = options.contains(.flipOnVerticalAxis) ? -1.0 : 1.0 + let y = options.contains(.flipOnHorizontalAxis) ? 1.0 : -1.0 + renderContext.cgContext.scaleBy(x: CGFloat(x), y: CGFloat(y)) + + let drawRect = CGRect(origin: CGPoint(x: -cgImageSize.width / 2.0, y: -cgImageSize.height / 2.0), size: cgImageSize) + renderContext.cgContext.draw(cgImage, in: drawRect) + } + + return image + } + + /// Rotates the image based on the information collected by the accelerometer + func withFixedOrientation() -> UIImage { + var imageAngle: Double = 0.0 + + var shouldRotate = true + switch CaptureSession.current.editImageOrientation { + case .up: + shouldRotate = false + case .left: + imageAngle = Double.pi / 2 + case .right: + imageAngle = -(Double.pi / 2) + case .down: + imageAngle = Double.pi + default: + shouldRotate = false + } + + if shouldRotate, + let finalImage = rotated(by: Measurement(value: imageAngle, unit: .radians)) { + return finalImage + } else { + return self + } + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+SFSymbol.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+SFSymbol.swift new file mode 100644 index 0000000..e6465aa --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+SFSymbol.swift @@ -0,0 +1,27 @@ +// +// UIImage+SFSymbol.swift +// WeScan +// +// Created by André Schmidt on 19/06/2020. +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit + +extension UIImage { + + /// Creates an image object containing a system symbol image appropriate for the specified traits if supported (iOS13 and above). + /// Otherwise an image object using the named image asset that is compatible with the specified trait collection will be created. + convenience init?( + systemName: String, + named: String, + in bundle: Bundle? = nil, + compatibleWith traitCollection: UITraitCollection? = nil + ) { + if #available(iOS 13.0, *) { + self.init(systemName: systemName, compatibleWith: traitCollection) + } else { + self.init(named: named, in: bundle, compatibleWith: traitCollection) + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Utils.swift b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Utils.swift new file mode 100644 index 0000000..2fdabc6 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Extensions/UIImage+Utils.swift @@ -0,0 +1,147 @@ +// +// UIImage+Utils.swift +// WeScan +// +// Created by Bobo on 5/25/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +extension UIImage { + /// Draws a new cropped and scaled (zoomed in) image. + /// + /// - Parameters: + /// - point: The center of the new image. + /// - scaleFactor: Factor by which the image should be zoomed in. + /// - size: The size of the rect the image will be displayed in. + /// - Returns: The scaled and cropped image. + func scaledImage(atPoint point: CGPoint, scaleFactor: CGFloat, targetSize size: CGSize) -> UIImage? { + guard let cgImage = self.cgImage else { return nil } + + let scaledSize = CGSize(width: size.width / scaleFactor, height: size.height / scaleFactor) + let midX = point.x - scaledSize.width / 2.0 + let midY = point.y - scaledSize.height / 2.0 + let newRect = CGRect(x: midX, y: midY, width: scaledSize.width, height: scaledSize.height) + + guard let croppedImage = cgImage.cropping(to: newRect) else { + return nil + } + + return UIImage(cgImage: croppedImage) + } + + /// Scales the image to the specified size in the RGB color space. + /// + /// - Parameters: + /// - scaleFactor: Factor by which the image should be scaled. + /// - Returns: The scaled image. + func scaledImage(scaleFactor: CGFloat) -> UIImage? { + guard let cgImage = self.cgImage else { return nil } + + let customColorSpace = CGColorSpaceCreateDeviceRGB() + + let width = CGFloat(cgImage.width) * scaleFactor + let height = CGFloat(cgImage.height) * scaleFactor + let bitsPerComponent = cgImage.bitsPerComponent + let bytesPerRow = cgImage.bytesPerRow + let bitmapInfo = cgImage.bitmapInfo.rawValue + + guard let context = CGContext( + data: nil, + width: Int(width), + height: Int(height), + bitsPerComponent: bitsPerComponent, + bytesPerRow: bytesPerRow, + space: customColorSpace, + bitmapInfo: bitmapInfo + ) else { return nil } + + context.interpolationQuality = .high + context.draw(cgImage, in: CGRect(origin: .zero, size: CGSize(width: width, height: height))) + + return context.makeImage().flatMap { UIImage(cgImage: $0) } + } + + /// Returns the data for the image in the PDF format + func pdfData() -> Data? { + // Typical Letter PDF page size and margins + let pageBounds = CGRect(x: 0, y: 0, width: 595, height: 842) + let margin: CGFloat = 40 + + let imageMaxWidth = pageBounds.width - (margin * 2) + let imageMaxHeight = pageBounds.height - (margin * 2) + + let image = scaledImage(scaleFactor: size.scaleFactor(forMaxWidth: imageMaxWidth, maxHeight: imageMaxHeight)) ?? self + let renderer = UIGraphicsPDFRenderer(bounds: pageBounds) + + let data = renderer.pdfData { ctx in + ctx.beginPage() + + ctx.cgContext.interpolationQuality = .high + + image.draw(at: CGPoint(x: margin, y: margin)) + } + + return data + } + + /// Function gathered from [here](https://stackoverflow.com/questions/44462087/how-to-convert-a-uiimage-to-a-cvpixelbuffer) + /// to convert UIImage to CVPixelBuffer + /// + /// - Returns: new [CVPixelBuffer](apple-reference-documentation://hsVf8OXaJX) + func pixelBuffer() -> CVPixelBuffer? { + let attrs = [ + kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue, + kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue + ] as CFDictionary + var pixelBufferOpt: CVPixelBuffer? + let status = CVPixelBufferCreate( + kCFAllocatorDefault, + Int(self.size.width), + Int(self.size.height), + kCVPixelFormatType_32ARGB, + attrs, + &pixelBufferOpt + ) + guard status == kCVReturnSuccess, let pixelBuffer = pixelBufferOpt else { + return nil + } + + CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) + + let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + guard let context = CGContext( + data: pixelData, + width: Int(self.size.width), + height: Int(self.size.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), + space: rgbColorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue + ) else { + return nil + } + + context.translateBy(x: 0, y: self.size.height) + context.scaleBy(x: 1.0, y: -1.0) + + UIGraphicsPushContext(context) + self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)) + UIGraphicsPopContext() + CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) + + return pixelBuffer + } + + /// Creates a UIImage from the specified CIImage. + static func from(ciImage: CIImage) -> UIImage { + if let cgImage = CIContext(options: nil).createCGImage(ciImage, from: ciImage.extent) { + return UIImage(cgImage: cgImage) + } else { + return UIImage(ciImage: ciImage, scale: 1.0, orientation: .up) + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/ImageScannerController.swift b/ios/WeScan-3.0.0/Sources/WeScan/ImageScannerController.swift new file mode 100644 index 0000000..978c1e1 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/ImageScannerController.swift @@ -0,0 +1,214 @@ +// +// ImageScannerController.swift +// WeScan +// +// Created by Boris Emorine on 2/12/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import UIKit + +/// A set of methods that your delegate object must implement to interact with the image scanner interface. +public protocol ImageScannerControllerDelegate: NSObjectProtocol { + + /// Tells the delegate that the user scanned a document. + /// + /// - Parameters: + /// - scanner: The scanner controller object managing the scanning interface. + /// - results: The results of the user scanning with the camera. + /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. + func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) + + /// Tells the delegate that the user cancelled the scan operation. + /// + /// - Parameters: + /// - scanner: The scanner controller object managing the scanning interface. + /// - Discussion: Your delegate's implementation of this method should dismiss the image scanner controller. + func imageScannerControllerDidCancel(_ scanner: ImageScannerController) + + /// Tells the delegate that an error occurred during the user's scanning experience. + /// + /// - Parameters: + /// - scanner: The scanner controller object managing the scanning interface. + /// - error: The error that occurred. + func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) +} + +/// A view controller that manages the full flow for scanning documents. +/// The `ImageScannerController` class is meant to be presented. It consists of a series of 3 different screens which guide the user: +/// 1. Uses the camera to capture an image with a rectangle that has been detected. +/// 2. Edit the detected rectangle. +/// 3. Review the cropped down version of the rectangle. +public final class ImageScannerController: UINavigationController { + + /// The object that acts as the delegate of the `ImageScannerController`. + public weak var imageScannerDelegate: ImageScannerControllerDelegate? + + // MARK: - Life Cycle + + /// A black UIView, used to quickly display a black screen when the shutter button is presseed. + internal let blackFlashView: UIView = { + let view = UIView() + view.backgroundColor = UIColor(white: 0.0, alpha: 0.5) + view.isHidden = true + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + override public var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .portrait + } + + public required init(image: UIImage? = nil, delegate: ImageScannerControllerDelegate? = nil) { + super.init(rootViewController: ScannerViewController()) + + self.imageScannerDelegate = delegate + + if #available(iOS 13.0, *) { + navigationBar.tintColor = .label + } else { + navigationBar.tintColor = .black + } + navigationBar.isTranslucent = false + self.view.addSubview(blackFlashView) + setupConstraints() + + // If an image was passed in by the host app (e.g. picked from the photo library), use it instead of the document scanner. + if let image { + detect(image: image) { [weak self] detectedQuad in + guard let self else { return } + let editViewController = EditScanViewController(image: image, quad: detectedQuad, rotateImage: false) + self.setViewControllers([editViewController], animated: false) + } + } + } + + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + public required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func detect(image: UIImage, completion: @escaping (Quadrilateral?) -> Void) { + // Whether or not we detect a quad, present the edit view controller after attempting to detect a quad. + // *** Vision *requires* a completion block to detect rectangles, but it's instant. + // *** When using Vision, we'll present the normal edit view controller first, then present the updated edit view controller later. + + guard let ciImage = CIImage(image: image) else { return } + let orientation = CGImagePropertyOrientation(image.imageOrientation) + let orientedImage = ciImage.oriented(forExifOrientation: Int32(orientation.rawValue)) + + if #available(iOS 11.0, *) { + // Use the VisionRectangleDetector on iOS 11 to attempt to find a rectangle from the initial image. + VisionRectangleDetector.rectangle(forImage: ciImage, orientation: orientation) { quad in + let detectedQuad = quad?.toCartesian(withHeight: orientedImage.extent.height) + completion(detectedQuad) + } + } else { + // Use the CIRectangleDetector on iOS 10 to attempt to find a rectangle from the initial image. + let detectedQuad = CIRectangleDetector.rectangle(forImage: ciImage)?.toCartesian(withHeight: orientedImage.extent.height) + completion(detectedQuad) + } + } + + public func useImage(image: UIImage) { + guard topViewController is ScannerViewController else { return } + + detect(image: image) { [weak self] detectedQuad in + guard let self else { return } + let editViewController = EditScanViewController(image: image, quad: detectedQuad, rotateImage: false) + self.setViewControllers([editViewController], animated: true) + } + } + + public func resetScanner() { + setViewControllers([ScannerViewController()], animated: true) + } + + private func setupConstraints() { + let blackFlashViewConstraints = [ + blackFlashView.topAnchor.constraint(equalTo: view.topAnchor), + blackFlashView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + view.bottomAnchor.constraint(equalTo: blackFlashView.bottomAnchor), + view.trailingAnchor.constraint(equalTo: blackFlashView.trailingAnchor) + ] + + NSLayoutConstraint.activate(blackFlashViewConstraints) + } + + internal func flashToBlack() { + view.bringSubviewToFront(blackFlashView) + blackFlashView.isHidden = false + let flashDuration = DispatchTime.now() + 0.05 + DispatchQueue.main.asyncAfter(deadline: flashDuration) { + self.blackFlashView.isHidden = true + } + } +} + +/// Data structure containing information about a scan, including both the image and an optional PDF. +public struct ImageScannerScan { + public enum ImageScannerError: Error { + case failedToGeneratePDF + } + + public var image: UIImage + + public func generatePDFData(completion: @escaping (Result) -> Void) { + DispatchQueue.global(qos: .userInteractive).async { + if let pdfData = self.image.pdfData() { + completion(.success(pdfData)) + } else { + completion(.failure(.failedToGeneratePDF)) + } + } + + } + + mutating func rotate(by rotationAngle: Measurement) { + guard rotationAngle.value != 0, rotationAngle.value != 360 else { return } + image = image.rotated(by: rotationAngle) ?? image + } +} + +/// Data structure containing information about a scanning session. +/// Includes the original scan, cropped scan, detected rectangle, and whether the user selected the enhanced scan. +/// May also include an enhanced scan if no errors were encountered. +public struct ImageScannerResults { + + /// The original scan taken by the user, prior to the cropping applied by WeScan. + public var originalScan: ImageScannerScan + + /// The deskewed and cropped scan using the detected rectangle, without any filters. + public var croppedScan: ImageScannerScan + + /// The enhanced scan, passed through an Adaptive Thresholding function. + /// This image will always be grayscale and may not always be available. + public var enhancedScan: ImageScannerScan? + + /// Whether the user selected the enhanced scan or not. + /// The `enhancedScan` may still be available even if it has not been selected by the user. + public var doesUserPreferEnhancedScan: Bool + + /// The detected rectangle which was used to generate the `scannedImage`. + public var detectedRectangle: Quadrilateral + + init( + detectedRectangle: Quadrilateral, + originalScan: ImageScannerScan, + croppedScan: ImageScannerScan, + enhancedScan: ImageScannerScan?, + doesUserPreferEnhancedScan: Bool = false + ) { + self.detectedRectangle = detectedRectangle + + self.originalScan = originalScan + self.croppedScan = croppedScan + self.enhancedScan = enhancedScan + + self.doesUserPreferEnhancedScan = doesUserPreferEnhancedScan + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Protocols/CaptureDevice.swift b/ios/WeScan-3.0.0/Sources/WeScan/Protocols/CaptureDevice.swift new file mode 100644 index 0000000..11e0462 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Protocols/CaptureDevice.swift @@ -0,0 +1,62 @@ +// +// CaptureDevice.swift +// WeScan +// +// Created by Julian Schiavo on 28/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import Foundation + +protocol CaptureDevice: AnyObject { + var torchMode: AVCaptureDevice.TorchMode { get set } + var isTorchAvailable: Bool { get } + + var focusMode: AVCaptureDevice.FocusMode { get set } + var focusPointOfInterest: CGPoint { get set } + var isFocusPointOfInterestSupported: Bool { get } + + var exposureMode: AVCaptureDevice.ExposureMode { get set } + var exposurePointOfInterest: CGPoint { get set } + var isExposurePointOfInterestSupported: Bool { get } + + var isSubjectAreaChangeMonitoringEnabled: Bool { get set } + + func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool + func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool + func unlockForConfiguration() + func lockForConfiguration() throws +} + +extension AVCaptureDevice: CaptureDevice { } + +final class MockCaptureDevice: CaptureDevice { + var torchMode: AVCaptureDevice.TorchMode = .off + var isTorchAvailable = true + + var focusMode: AVCaptureDevice.FocusMode = .continuousAutoFocus + var focusPointOfInterest: CGPoint = .zero + var isFocusPointOfInterestSupported = true + + var exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure + var exposurePointOfInterest: CGPoint = .zero + var isExposurePointOfInterestSupported = true + var isSubjectAreaChangeMonitoringEnabled = false + + func unlockForConfiguration() { + return + } + + func lockForConfiguration() throws { + return + } + + func isFocusModeSupported(_ focusMode: AVCaptureDevice.FocusMode) -> Bool { + return true + } + + func isExposureModeSupported(_ exposureMode: AVCaptureDevice.ExposureMode) -> Bool { + return true + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Protocols/Transformable.swift b/ios/WeScan-3.0.0/Sources/WeScan/Protocols/Transformable.swift new file mode 100644 index 0000000..05e4b63 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Protocols/Transformable.swift @@ -0,0 +1,42 @@ +// +// Extendable.swift +// WeScan +// +// Created by Boris Emorine on 2/15/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +/// Objects that conform to the Transformable protocol are capable of being transformed with a `CGAffineTransform`. +protocol Transformable { + + /// Applies the given `CGAffineTransform`. + /// + /// - Parameters: + /// - t: The transform to apply + /// - Returns: The same object transformed by the passed in `CGAffineTransform`. + func applying(_ transform: CGAffineTransform) -> Self + +} + +extension Transformable { + + /// Applies multiple given transforms in the given order. + /// + /// - Parameters: + /// - transforms: The transforms to apply. + /// - Returns: The same object transformed by the passed in `CGAffineTransform`s. + func applyTransforms(_ transforms: [CGAffineTransform]) -> Self { + + var transformableObject = self + + transforms.forEach { transform in + transformableObject = transformableObject.applying(transform) + } + + return transformableObject + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance.png new file mode 100644 index 0000000000000000000000000000000000000000..e87fc945f9919c1632d925e557da7adb81f883b4 GIT binary patch literal 511 zcmVES@%!JqclX@m+-)D7iB&&)?X~y%uYK0uS331~J0`Um&Rr6fuEiwQ zdZ&4g9Y$8INOR=hr14=#ZZZ-0&Acm|HITa@vB9>0wtwk3R*S6?%*iz6)t-VU%7Me zXyO`lC)3cNi7ZnXe*D!XjYFSVsbN$By`+5@2AK*?z`G_`HB5q@c5IXQ6?_Fa#rJ9z z`bK0!sI#C2yIa1Zw0aNQgqzIeVZN*JWa52P?kAjuzO3ptusxU`0;A>hYJs`HI5gSrTp`e4!`A^cAs8 z;`&5;a*1mm)z?kpv*2wKk(k<{wz$Mu&i|+~a(pZ-t|H}5?xR<-N4?br>b2MlO-x*s zRzd$uhCsbfR2;oj2jt)tu0ice+}csp`|VCz`5WVTPAN1UFQEVc002ovPDHLkV1i3+ B<|6wOIV4|Sll^B?LYocJXsiN5`;)R-Mpy2WS&*^1oW_o6NrpNd675w(=qpJR@shRF= z8;$ycaSGa2ZTZb5vq{GrAAJ z>Jm1bsymp~S4Fr6vVu4et$jy32e@P-lkb6SkAP{Qa+!|Bb0BXJJ*+(oj)N&6P2%J~ zuxJZVsRj|1ksU;tY}w>D1z~L&O$uU?wI)w@EsH(^JyB)87V`LMih?)~eu81(^JQ9w z-c#@y)Jh(IO;KHMjeh$!-6*c}Rqp6@GEg7`6AG-YV(A);ECH{9Ui8{U8;LfqnF*M| zSlZDAnnC3LBU_O2>Ahj zH(@kIUNvvZ!`1#5 z?_$YhJ}-GWHvx?DJ1YRK*G1V*ps$m+B$1yVS+mIxWq2tXK!&;O=_K+d^9xYdy`?Fe zEb{VJYXBmzD?md~Wn(h$Lmu_U{>a`d+B_vB9WMz%UPSf+UF?nk*H?&Xf3eBm!&F-4 zj}>&Imp~V*mgE literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance@3x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/enhance@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5a58b69fc417df613963a7b15f4ccdd473c58a6f GIT binary patch literal 1332 zcmV-417rKy@aU(l-DuQAVeSjKQE}R7f{C?>3EIM^>SC6;*b)DpYr@O1V z>eP2nRS);R!Js>~m{5O5=M!zk*KU{9-b2Sbh=4Lazz8S;${>(Y95W~a${>(Y+++ql zODr#C#M4yqeRQ-7UDH&DS^Ydig;PlPFhwC%bsuyWF&s@|`4E3C8mq0Q_cO?D@XoPa z1X2}%TsRibZsM7N#%6Qr`E6tY$rphnKz};zyv}wW^TGL+23F*0q^`P`)3D}z8Ax+D z|55Z>z1-UITr^E+Tg=5=1$3qU?NTsJkDppzNHX1vN z?2{hl-wWtiaDK{Y@3nw-d{y+iWK?{YlvVXkT8;kgi^g+I(`1xAMB)2r;iyWKy@_7Q zsN*Zv%er9G^H3EgK!2eS;HO4(C^(u{S#Ye0H`q9UKD7e@UVuVB%t2Lvex%b!Pza#k zh72Mv95ti(Cr}XZpi)|zw4+CY8Vf4);AEf8!OIN#9U4F&q_%qeBye8(v7 z?aW9E1iG4YicYKVBb@)&7+Y)1_X3WtdN$xZKEfFRF2c!xqhDkcF?}KMWRVk_Jm*0= zD>JQ%!qG$YM@Z9n18Sv>3(nQh_!!w5r|-vF2WebzlqpxDcaZ^-ZxtjToX-gIEp$2+ zxH|}KYe6nLtvGecX0f#wx$U>rQ~Kwg=nfw?9)N03`@lKo+sW9KRZ%l{S3#JA(8M(woh-sCLT(=WC+Ln>jXsssO3IE6{{BT&E9(vxFeZz|k101XL%J zskVJ^wD6|a93PzH)XQ3PraDSCIt8dKocq(G57_SvpmD$vzfJ&JNucRVd7f!iBy%je z1n3WBi~LqAXp0w|lgI*;7aV=GCXXL9MYoaK0jH7vgdD%fbkB>nxmmPZ1(;rb`PQ6XWX=Tw z$)8R5^bf(6Nr#XNPB)LegTt6F*k=BoLoN_^yVr0oA{q>aL5gsU5dKB<53=kz%Jxds qOLMPQE&%-kbprj0_M@LsFYP}=O17e<&(U%K0000@KKxDRV{V1!s17w zQ%;;q+f>PPQaC~e zn3}RO_{QPZ8ojB%3flJvyDoUOr9C?v2x#n>~AV{I&9rovZGr}U1UKJ_d8bK z6{c;rH4IyoEDrI^SH2MRchv!&|CuZ6dKC_bPS2XOzmk3D0!8*)qKY8y_10fOZ-Y8) f<^SuQ-_88|R`klbu`7XoXJGJj^>bP0l+XkKAOebM literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@2x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1c21bba43c8bea1ec11f150f3a5a00f054f2bb9d GIT binary patch literal 555 zcmV+`0@VG9P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0l-N_K~!i%?b*94 z#bF%B@gpK729dHTVX#PKF)OiI31 zMsTgc8k@!ziqZtE0cq_Z^ZM`(Yd{+MHM zq&1Js>xA{e)6%OGWyn1J6kWh(9!_A5O}awnM_Wkium<~zau75cMceSlM{%?+y$byZH~s@r9IdM#{%3y{QRV;dE1IMAyMe}EBATOh tX|TrYXxsEajOm{lJ-=_3Diu+=+&2VZZtYT)^Z)<=002ovPDHLkV1i$c`cD7= literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@3x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flash@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..acae8b959cf215693c8e0b8128b4c6c60dda3e5f GIT binary patch literal 798 zcmV+(1L6FMP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0<%d(K~#8N?c2+% zO<@?v@$FLXg&Db%nTeSqx7;WG12ZLZNlD7YOevj`k%3E*WJHNFVMHzwDH)oOq!hXP z{!Zu3+w!cnw_~?@+t2f@&)}TVT2K2tr*+w#D2k$}=f+_R77YIs{nZ)#!Us&(FQPx~ zhK7^OLNwAUd_fw{b3`M}!5ieCo)d^@67C}(j%+|gXHgx_M@)eG_Mti)xd!*G!8cd~ z+6C9m!&_Lxk%e&EWITj57?yV^{;J+5>l8f;9}wVz}xctU*{_!d1H1?6#D1LvYg~yn{7}I$Pl)E#)V$hEPZU z-gVn!;2VR>u!hj&23&Lq)&RQMi-B(w{$(lWmSUh=g7>gazMEGV=%(N)tdsA>DGY2{ z%2!~WcpL6wDvIq0tOIY;3KW^1pO1Da_sb)UM{#K>>)E+=&~4G}lldsFX?ODcbChnuI`Li6u*@FmG0GihBbsb=i#R9u!c~_#Zu0*-e9fcW+|t}qWW@p9W7a~>T!gcx*oW2ZK9gh2@oZ5`)aK!ae&O43j zaCE^MkseFGMm`)ZWfyO+_R@0X!-=I_=@`;*zG5|^tcyqkiluC{4Ej3v;o+a6D2k%! c|Lb&q1Dx(UBd64DaR2}S07*qoM6N<$f<88d^Z)<= literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable.png new file mode 100644 index 0000000000000000000000000000000000000000..38204fb2bc3816f0d72fb278892d3d2d03f1722b GIT binary patch literal 395 zcmV;60d)R}P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0U$|4K~zXf&D1eV zLQxpT@tY0}IXbvCWVo8NMlX(Gbxb1kqO1|GBpp zrK^Kgv#6wnPgubYa%Hw;7jLlP4~P;gd4vs@8$^jNRVA~ZUQQ4t zR~TSXiR%FvslT? z?`!VpPwgF6pi3<9#x(i4H8h)Mb{*Hzp pX3OPsiQp$SBo&mh<=Q1legImZVenk0gUbK_002ovPDHLkV1f=Lrvm^0 literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@2x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..585bed6ef3577d193f444e76475598cfc8ff4e36 GIT binary patch literal 669 zcmV;O0%HA%P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0y0TNK~!i%?b$y_ z++ZBW@hipJf>=7(Rs;tJ5f=-B(8pQ@lLFPt4xz6)bnL zjVpZAfudy*8eS%Kz-Kx!gZ7)f^clw`Oo48A%`|kP-*~74DKdaFnBo7xdo;9$Pg|k` z2Qb6S5;95J61r%=!5&^lkXh1{ScW_NDO!M8LG?(p&jxulj7gRn1JoA-~;xFji| zi}n!i;6@K}OO@fSg)Qi(OVL)%smOxzN$Z@1&TKv1zuRCA8|2rfxWd_{24 zomNRwVhX`!6>rO%HT^rq*+3?lfH`R+OMHO$4B-Oyi5pR35Bhtrd4eNE4Une93HsnQ z_n-xN)Br|3CA3l-h4-u@Y5-TQCFbBg{fIhwB}q$c;tss$0T%Esf$z|8)ktltF} z`=PE<30<_k2r~a*2CYBxD;bk6i38F+B+$U7|gd}&`+)KQKJAAE=$GMhx4}17} zACGe>p|$Zb%<#Hs$53zV62G7qW?nN4GrUy0Xp=-GH2i-&)d8Q;7Zzst&uG-+u@+E| z%bR|)4-KEmnzqr=z#2Z2HLb7c%Z%sc&1RY@P)en1UDSwnMvsMw00000NkvXXu0mjf Dk69>M literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@3x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/flashUnavailable@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3202abee780b5a48f7a6d22aae0cd0165942beb4 GIT binary patch literal 940 zcmV;d15^BoP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D13^hdK~#8N?c2-i zjX@m8@gup*LP;T)kfN-V63V^U_z!GQV&RsR+_I9gR7#X0Sx{J5SlKAK#KOXwa!Eo; zxxK%~@5#3@=XvJ2{$}RPr(We8e#gwGW1eH?IlYvWl#~?bjKLNx=s!yNFX!Qcc=SUgJ9K3;k_nd)uk%2D%`^(NU9uJ|*5$oVpX5%Y<;kZ1} zQ7*zP=QGB^t1N?*lMV7D+Rk3+Vlp@3Wllg3w9S&%LNChNZg?9hMGv-(l4PxW2mSYG zbK!N`ENK!RLBGEC0zss$k}gBPuIVg-Nz*0mhkiYq^$03Wl_Z~Qen7vL=?f+xxYR9a z9`Y9D%q;|)swGXv6X>5EWDkN)m6EPO|4bh5FYhiXQ_>;mKbz+zqKQjMn^3eUYe8M! zU6NPQLVSS!{UVzYU2G-EqAZ(T`uQTkUfx||mh{Sa?BWihj#*M!!C&58Qm3S$IpV2O zQdF0BTVzjJr)){*5N+fX^g7H*)?JfaJ-}3Wn`OmO-S&UDx4cbK$CCQ%le0Z7runoG5Iwi?FcLTi5J=g|bR@IUY!0SxM z4#-UfRp^4tmUIQt$GkzxwWP=Ja#3zFv+xt<p zY12@bBzraApeJlIv?Xnao@kz-Ea?*TB=g88W%JJZs7o4)x6l(DByam7+LBg7Pb^Q- z_C=H>$yW*Vqz1VPmqb^RoOkI7*_7SBh^C|^&=awdJ>@Ff7g3aS7x_ipOmm~*z zdIF{<_LPGyX$JDPW}Dno4zeWKH`0UF+SpSLt|a*?lOAxU&4a#RN}33{olXx{)LoQa zVwSWI-enu~ATx41a2L@f-IgbO%LV9SMArv>@-4=@SkZsvQRGYRUyN2xBP&N1Wx3NnMMZyUrK?3bs4RtS&>!nRN=ZpcNlB@nUhfZY5pzKO@d|Nn~YUV2sQR zi71Ki^|4CM&(%vz$xlkvtH>8?tx|+mh4pF`@Az(8j6ba4#v;C(yQo|~~y;&}fQo>K}b z7Yu)Gj^JTiEUqxS^=kA7zCEnX7rhUtbQ>j1;c*kuRDSqbMD_2S-Nn-Pf0dM<|6IH8 zuI=^GS!rt+-fA!D$-TDA>Kk*;!_^7mjEh)A9x@eex1M0VeT|8*^Nqt?&kEQ$ZJoqz z5~6tL7{#=GsP%fTkr?#enZ;*yOR${&HzUF4QB3R)STA!uigETce7HEQ;rKjJvC@@J zI~V8OT)j*3^r0*Fc2s%YOA)H-Dr<}4R_nUAd(sor)bPrPLu~U@^;hsde*UPv_@~1w zH#yF&i&ww7Qux69Uo|CLQ}nTA&E Sr4yx~#O3Mg=d#Wzp$P#0A@`I3 literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@2x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..61a6ffcbd3ca4707a09d6572431915e299390a95 GIT binary patch literal 1006 zcmeAS@N?(olHy`uVBq!ia0vp^T0m^X!3JWvd9DRgEa{HEjtmSN`?>!lvNA9*MrMXY zltlRYSS9D@>LsS+C#C9DVstT4fPE4;bsH1+JHo@{EISEfi{E8w==W>t3(ll z+GC>+vK+}V5TAlYfnK%aveAbJn;nfAXLPL=&>CyG~ zZ}v@ejhJ)O{PU(mF&4(wyPw~w{_DN49-rNV3jg^L-##!RV7wf30c%16hIP@-Mhk zgqTAvuDr+o?7@w5#gP_o7N7W~TWr0?{D9OChJ6jUm+4RXUT0k1xWLf0EY@h|4QAs7 zKa4h?X}`I6`kEuA>{*kqpa0y`a9C((Xvop$j%Wqq4=t5P zlvWyM`N%Ec%=^Uas@Gs;$&$0^#tXBi2WqLWv;)%9wr=Q&tGJZY=;Onu&RTD9ond+f z>&|TfFP9=9!p?sdC~! zJ=99$ufH`)DVY?rHN^NjgP6m1F|mc-3lCpUKIrQm$a~dlm6r14mad{LtV?gtSid~D zUSCP-YwgS=_hk3l@&o1jcHn26e&HZyKRrL|g=c5>ieE)M6ORkC1{%8JCiYshd@t&FU*MYL7r>mdKI;Vst E0QCZuY5)KL literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@3x.png b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Assets/rotate@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9bffb26df0452a3b9e2013f12850f788cf0c006e GIT binary patch literal 1318 zcmeAS@N?(olHy`uVBq!ia0vp^_CV~-!3HFczlrAnQY`6?zK#qG8~eHcB(gFvFh*vE zM3hAM`dB6B=jtV<RtPXT0ZVp4u- ziLH_n$Rap^xU(cP4PjGWG1OZ?59)(t^bPe4^xwq{Aay6cg!cx-TnN|=X&}*5BD4Ymo-&*F03-wCG_S8 zmHt)5cUbsYDxV3t&Mt|2@g?B^o52p2Jm$@p8uE-*NNN}Sl}f1WOFePv3a^AbV^@-l z`b~~w`VVXhTG{6{^WNr)VVTEhH1~DdJN9nI?Hk$8$T63;ipBbW{nznaVcWNhT_udK(fH##@$ z>puK&i^i?Dk@;HrKmdv?b(@5Xvgv_~rUF_nb zVwcW%UG>|OzIw~pM`iiyGzQK5G(T#wl+({dwtN?rV<`d3`G>b2Z|rSY9(m-u$oa-- zhVMIcb{sqUbC-Vc^Z@Vunv-_TGx|9(U2M-<(W25L{41=2&KJu)*iz5`q4-N&g2W1` z^sBAT67j4SSNE_?SFl%qGF$i9&XXoP?an_~5bqaqB4>fcS+^>lPeOf*@3!$oh8)_o z?#h-^!8T97U0YQUQab6!iZ@0ov*(!Sq=hV<^f~Zms)w0n{*5y$yi}hr-gJ6GPSL#$ zGXtl3opVo}u6Xmwo!rrTo;`wv@@A+s^i^3MsPF+RbxP z?K$sk$=Pg_*!8)3D6|H-YlS1i1iYO?;} zCVO>V=O%mg$m#YU^ganE*w}xYQ00)+t>m;X?P9xEKEuyd)1Ujydy*}s9oW0>v-m~1 yqm#vfp|YVjQ!r~iJJ|0JxD(EBFN}QpQGDi;wmr9>99sb@&OBZHT-G@yGywqeU?v>^ literal 0 HcmV?d00001 diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ar.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ar.lproj/Localizable.strings new file mode 100644 index 0000000..31508f0 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ar.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "التالي"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "تعديل الصورة"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "عرض"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "إلغاء"; +"wescan.scanning.auto" = "تلقائي"; +"wescan.scanning.manual" = "يدوي"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/cs.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/cs.lproj/Localizable.strings new file mode 100644 index 0000000..c5eca81 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/cs.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Martin Georgiu on 3/8/20. + Copyright © 2020 Martin Georgiu. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Pokračovat"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Upravit"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Náhled"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Zpět"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Ručně"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/de.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/de.lproj/Localizable.strings new file mode 100644 index 0000000..6ae9dfb --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/de.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. + */ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Weiter"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Scan editieren"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Überprüfen"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Abbrechen"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manuell"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/en.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/en.lproj/Localizable.strings new file mode 100644 index 0000000..ec64598 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/en.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Next"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Edit Scan"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Review"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Cancel"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manual"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es-419.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es-419.lproj/Localizable.strings new file mode 100644 index 0000000..2a4e04b --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es-419.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Nicolas Garcia on 3/9/20. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Siguiente"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Editar Escaneo"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Revisión"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Cancelar"; +"wescan.scanning.auto" = "Automático"; +"wescan.scanning.manual" = "Manual"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es.lproj/Localizable.strings new file mode 100644 index 0000000..2a4e04b --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/es.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Nicolas Garcia on 3/9/20. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Siguiente"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Editar Escaneo"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Revisión"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Cancelar"; +"wescan.scanning.auto" = "Automático"; +"wescan.scanning.manual" = "Manual"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/fr.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/fr.lproj/Localizable.strings new file mode 100644 index 0000000..b05f258 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/fr.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. + */ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Suivant"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Modifier"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Aperçu"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Annuler"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manuel"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/hu.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/hu.lproj/Localizable.strings new file mode 100644 index 0000000..f2d47fb --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/hu.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + Localizable.strings + WeScan + + Created by Rénes Péter on 2019. 07. 08.. + Copyright © 2019. WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Következő"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Szerkesztés"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Előnézet"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Mégsem"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Kézi"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/it.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/it.lproj/Localizable.strings new file mode 100644 index 0000000..ed4b186 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/it.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Avanti"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Modifica Scansione"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Analisi"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Annulla"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manuale"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ko.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ko.lproj/Localizable.strings new file mode 100644 index 0000000..011d367 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ko.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "다음"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "스캔 수정"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "검토"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "취소"; +"wescan.scanning.auto" = "자동"; +"wescan.scanning.manual" = "수동"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/nl.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/nl.lproj/Localizable.strings new file mode 100644 index 0000000..2dd1407 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/nl.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Volgende"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Wijzig Scan"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Beoordelen"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Annuleren"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Handmatig"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pl.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pl.lproj/Localizable.strings new file mode 100644 index 0000000..c689f9d --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pl.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Lukasz Szarkowicz on 06/03/2020. + Copyright © 2020 Lukasz Szarkowicz. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Dalej"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Edytuj dokument"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Podgląd"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Anuluj"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manualnie"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-BR.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-BR.lproj/Localizable.strings new file mode 100644 index 0000000..855713b --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-BR.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. + */ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Avançar"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Editar imagem"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Revisar"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Cancelar"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manual"; + diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-PT.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-PT.lproj/Localizable.strings new file mode 100644 index 0000000..44da26d --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/pt-PT.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Avançar"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Editar imagem"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Rever"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Cancelar"; +"wescan.scanning.auto" = "Auto"; +"wescan.scanning.manual" = "Manual"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ru.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ru.lproj/Localizable.strings new file mode 100644 index 0000000..133d3ff --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/ru.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Dmitriy Toropkin on 4/16/20. + Copyright © 2020 Dmitriy Toropkin. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Продолжить"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Редактирование"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Предпросмотр"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Отмена"; +"wescan.scanning.auto" = "Авто"; +"wescan.scanning.manual" = "Ручное"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/sv.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/sv.lproj/Localizable.strings new file mode 100644 index 0000000..73bfe69 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/sv.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Ola Nilsson on 12/8/20. + Copyright © 2020 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Nästa"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Redigera"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "Granska"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Avbryt"; +"wescan.scanning.auto" = "Automatisk"; +"wescan.scanning.manual" = "Manuell"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/tr.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/tr.lproj/Localizable.strings new file mode 100644 index 0000000..dede7c4 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/tr.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Hakan Eren on 30/04/2020. + Copyright © 2020 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "Sonraki"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "Düzenle"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "İncele"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "Vazgeç"; +"wescan.scanning.auto" = "Otomatik"; +"wescan.scanning.manual" = "Manuel"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hans.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..d136a22 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "下一步"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "编辑"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "回顾"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "取消"; +"wescan.scanning.auto" = "自动"; +"wescan.scanning.manual" = "手动"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hant.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hant.lproj/Localizable.strings new file mode 100644 index 0000000..e560c2f --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Resources/Localisation/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,21 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "下一步"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "編輯"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "回顧"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "取消"; +"wescan.scanning.auto" = "自動"; +"wescan.scanning.manual" = "手動"; diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Review/ReviewViewController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Review/ReviewViewController.swift new file mode 100644 index 0000000..4608c23 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Review/ReviewViewController.swift @@ -0,0 +1,183 @@ +// +// ReviewViewController.swift +// WeScan +// +// Created by Boris Emorine on 2/25/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import UIKit + +/// The `ReviewViewController` offers an interface to review the image after it +/// has been cropped and deskewed according to the passed in quadrilateral. +final class ReviewViewController: UIViewController { + + private var rotationAngle = Measurement(value: 0, unit: .degrees) + private var enhancedImageIsAvailable = false + private var isCurrentlyDisplayingEnhancedImage = false + + lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.clipsToBounds = true + imageView.isOpaque = true + imageView.image = results.croppedScan.image + imageView.backgroundColor = .black + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var enhanceButton: UIBarButtonItem = { + let image = UIImage( + systemName: "wand.and.rays.inverse", + named: "enhance", + in: Bundle(for: ScannerViewController.self), + compatibleWith: nil + ) + let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(toggleEnhancedImage)) + button.tintColor = .white + return button + }() + + private lazy var rotateButton: UIBarButtonItem = { + let image = UIImage(systemName: "rotate.right", named: "rotate", in: Bundle(for: ScannerViewController.self), compatibleWith: nil) + let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(rotateImage)) + button.tintColor = .white + return button + }() + + private lazy var doneButton: UIBarButtonItem = { + let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(finishScan)) + button.tintColor = navigationController?.navigationBar.tintColor + return button + }() + + private let results: ImageScannerResults + + // MARK: - Life Cycle + + init(results: ImageScannerResults) { + self.results = results + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + enhancedImageIsAvailable = results.enhancedScan != nil + + setupViews() + setupToolbar() + setupConstraints() + + title = NSLocalizedString("wescan.review.title", + tableName: nil, + bundle: Bundle(for: ReviewViewController.self), + value: "Review", + comment: "The review title of the ReviewController" + ) + navigationItem.rightBarButtonItem = doneButton + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // We only show the toolbar (with the enhance button) if the enhanced image is available. + if enhancedImageIsAvailable { + navigationController?.setToolbarHidden(false, animated: true) + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.setToolbarHidden(true, animated: true) + } + + // MARK: Setups + + private func setupViews() { + view.addSubview(imageView) + } + + private func setupToolbar() { + guard enhancedImageIsAvailable else { return } + + navigationController?.toolbar.barStyle = .blackTranslucent + + let fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbarItems = [fixedSpace, enhanceButton, flexibleSpace, rotateButton, fixedSpace] + } + + private func setupConstraints() { + imageView.translatesAutoresizingMaskIntoConstraints = false + + var imageViewConstraints: [NSLayoutConstraint] = [] + if #available(iOS 11.0, *) { + imageViewConstraints = [ + view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.topAnchor), + view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.trailingAnchor), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.bottomAnchor), + view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: imageView.safeAreaLayoutGuide.leadingAnchor) + ] + } else { + imageViewConstraints = [ + view.topAnchor.constraint(equalTo: imageView.topAnchor), + view.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), + view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor), + view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor) + ] + } + + NSLayoutConstraint.activate(imageViewConstraints) + } + + // MARK: - Actions + + @objc private func reloadImage() { + if enhancedImageIsAvailable, isCurrentlyDisplayingEnhancedImage { + imageView.image = results.enhancedScan?.image.rotated(by: rotationAngle) ?? results.enhancedScan?.image + } else { + imageView.image = results.croppedScan.image.rotated(by: rotationAngle) ?? results.croppedScan.image + } + } + + @objc func toggleEnhancedImage() { + guard enhancedImageIsAvailable else { return } + + isCurrentlyDisplayingEnhancedImage.toggle() + reloadImage() + + if isCurrentlyDisplayingEnhancedImage { + enhanceButton.tintColor = .yellow + } else { + enhanceButton.tintColor = .white + } + } + + @objc func rotateImage() { + rotationAngle.value += 90 + + if rotationAngle.value == 360 { + rotationAngle.value = 0 + } + + reloadImage() + } + + @objc private func finishScan() { + guard let imageScannerController = navigationController as? ImageScannerController else { return } + + var newResults = results + newResults.croppedScan.rotate(by: rotationAngle) + newResults.enhancedScan?.rotate(by: rotationAngle) + newResults.doesUserPreferEnhancedScan = isCurrentlyDisplayingEnhancedImage + imageScannerController.imageScannerDelegate? + .imageScannerController(imageScannerController, didFinishScanningWithResults: newResults) + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/CameraScannerViewController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/CameraScannerViewController.swift new file mode 100644 index 0000000..03cb3c4 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/CameraScannerViewController.swift @@ -0,0 +1,201 @@ +// +// CameraScannerViewController.swift +// WeScan +// +// Created by Chawatvish Worrapoj on 6/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import AVFoundation +import UIKit + +/// A set of methods that your delegate object must implement to get capture image. +/// If camera module doesn't work it will send error back to your delegate object. +public protocol CameraScannerViewOutputDelegate: AnyObject { + func captureImageFailWithError(error: Error) + func captureImageSuccess(image: UIImage, withQuad quad: Quadrilateral?) +} + +/// A view controller that manages the camera module and auto capture of rectangle shape of document +/// The `CameraScannerViewController` class is individual camera view include touch for focus, flash control, +/// capture control and auto detect rectangle shape of object. +public final class CameraScannerViewController: UIViewController { + + /// The status of auto scan. + public var isAutoScanEnabled: Bool = CaptureSession.current.isAutoScanEnabled { + didSet { + CaptureSession.current.isAutoScanEnabled = isAutoScanEnabled + } + } + + /// The callback to caller view to send back success or fail. + public weak var delegate: CameraScannerViewOutputDelegate? + + private var captureSessionManager: CaptureSessionManager? + private let videoPreviewLayer = AVCaptureVideoPreviewLayer() + + /// The view that shows the focus rectangle (when the user taps to focus, similar to the Camera app) + private var focusRectangle: FocusRectangleView! + + /// The view that draws the detected rectangles. + private let quadView = QuadrilateralView() + + /// Whether flash is enabled + private var flashEnabled = false + + override public func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + CaptureSession.current.isEditing = false + quadView.removeQuadrilateral() + captureSessionManager?.start() + UIApplication.shared.isIdleTimerDisabled = true + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + videoPreviewLayer.frame = view.layer.bounds + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + UIApplication.shared.isIdleTimerDisabled = false + captureSessionManager?.stop() + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return } + if device.torchMode == .on { + toggleFlash() + } + } + + private func setupView() { + view.backgroundColor = .darkGray + view.layer.addSublayer(videoPreviewLayer) + quadView.translatesAutoresizingMaskIntoConstraints = false + quadView.editable = false + view.addSubview(quadView) + setupConstraints() + + captureSessionManager = CaptureSessionManager(videoPreviewLayer: videoPreviewLayer) + captureSessionManager?.delegate = self + + NotificationCenter.default.addObserver( + self, + selector: #selector(subjectAreaDidChange), + name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, + object: nil + ) + } + + private func setupConstraints() { + var quadViewConstraints = [NSLayoutConstraint]() + + quadViewConstraints = [ + quadView.topAnchor.constraint(equalTo: view.topAnchor), + view.bottomAnchor.constraint(equalTo: quadView.bottomAnchor), + view.trailingAnchor.constraint(equalTo: quadView.trailingAnchor), + quadView.leadingAnchor.constraint(equalTo: view.leadingAnchor) + ] + NSLayoutConstraint.activate(quadViewConstraints) + } + + /// Called when the AVCaptureDevice detects that the subject area has changed significantly. When it's called, + /// we reset the focus so the camera is no longer out of focus. + @objc private func subjectAreaDidChange() { + /// Reset the focus and exposure back to automatic + do { + try CaptureSession.current.resetFocusToAuto() + } catch { + let error = ImageScannerControllerError.inputDevice + guard let captureSessionManager else { return } + captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error) + return + } + + /// Remove the focus rectangle if one exists + CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + guard let touch = touches.first else { return } + let touchPoint = touch.location(in: view) + let convertedTouchPoint: CGPoint = videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint) + + CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: false) + + focusRectangle = FocusRectangleView(touchPoint: touchPoint) + focusRectangle.setBorder(color: UIColor.white.cgColor) + view.addSubview(focusRectangle) + + do { + try CaptureSession.current.setFocusPointToTapPoint(convertedTouchPoint) + } catch { + let error = ImageScannerControllerError.inputDevice + guard let captureSessionManager else { return } + captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error) + return + } + } + + public func capture() { + captureSessionManager?.capturePhoto() + } + + public func toggleFlash() { + let state = CaptureSession.current.toggleFlash() + switch state { + case .on: + flashEnabled = true + case .off: + flashEnabled = false + case .unknown, .unavailable: + flashEnabled = false + } + } + + public func toggleAutoScan() { + isAutoScanEnabled.toggle() + } +} + +extension CameraScannerViewController: RectangleDetectionDelegateProtocol { + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error) { + delegate?.captureImageFailWithError(error: error) + } + + func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager) { + captureSessionManager.stop() + } + + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, + didCapturePicture picture: UIImage, + withQuad quad: Quadrilateral?) { + delegate?.captureImageSuccess(image: picture, withQuad: quad) + } + + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, + didDetectQuad quad: Quadrilateral?, + _ imageSize: CGSize) { + guard let quad else { + // If no quad has been detected, we remove the currently displayed on on the quadView. + quadView.removeQuadrilateral() + return + } + + let portraitImageSize = CGSize(width: imageSize.height, height: imageSize.width) + let scaleTransform = CGAffineTransform.scaleTransform(forSize: portraitImageSize, aspectFillInSize: quadView.bounds.size) + let scaledImageSize = imageSize.applying(scaleTransform) + let rotationTransform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0) + let imageBounds = CGRect(origin: .zero, size: scaledImageSize).applying(rotationTransform) + let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: imageBounds, toCenterOfRect: quadView.bounds) + let transforms = [scaleTransform, rotationTransform, translationTransform] + let transformedQuad = quad.applyTransforms(transforms) + quadView.drawQuadrilateral(quad: transformedQuad, animated: true) + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/CaptureSessionManager.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/CaptureSessionManager.swift new file mode 100644 index 0000000..d0ae615 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/CaptureSessionManager.swift @@ -0,0 +1,362 @@ +// +// CaptureManager.swift +// WeScan +// +// Created by Boris Emorine on 2/8/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import CoreMotion +import Foundation +import UIKit + +/// A set of functions that inform the delegate object of the state of the detection. +protocol RectangleDetectionDelegateProtocol: NSObjectProtocol { + + /// Called when the capture of a picture has started. + /// + /// - Parameters: + /// - captureSessionManager: The `CaptureSessionManager` instance that started capturing a picture. + func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager) + + /// Called when a quadrilateral has been detected. + /// - Parameters: + /// - captureSessionManager: The `CaptureSessionManager` instance that has detected a quadrilateral. + /// - quad: The detected quadrilateral in the coordinates of the image. + /// - imageSize: The size of the image the quadrilateral has been detected on. + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didDetectQuad quad: Quadrilateral?, _ imageSize: CGSize) + + /// Called when a picture with or without a quadrilateral has been captured. + /// + /// - Parameters: + /// - captureSessionManager: The `CaptureSessionManager` instance that has captured a picture. + /// - picture: The picture that has been captured. + /// - quad: The quadrilateral that was detected in the picture's coordinates if any. + func captureSessionManager( + _ captureSessionManager: CaptureSessionManager, + didCapturePicture picture: UIImage, + withQuad quad: Quadrilateral? + ) + + /// Called when an error occurred with the capture session manager. + /// - Parameters: + /// - captureSessionManager: The `CaptureSessionManager` that encountered an error. + /// - error: The encountered error. + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error) +} + +/// The CaptureSessionManager is responsible for setting up and managing the AVCaptureSession and the functions related to capturing. +final class CaptureSessionManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { + + private let videoPreviewLayer: AVCaptureVideoPreviewLayer + private let captureSession = AVCaptureSession() + private let rectangleFunnel = RectangleFeaturesFunnel() + weak var delegate: RectangleDetectionDelegateProtocol? + private var displayedRectangleResult: RectangleDetectorResult? + private var photoOutput = AVCapturePhotoOutput() + + /// Whether the CaptureSessionManager should be detecting quadrilaterals. + private var isDetecting = true + + /// The number of times no rectangles have been found in a row. + private var noRectangleCount = 0 + + /// The minimum number of time required by `noRectangleCount` to validate that no rectangles have been found. + private let noRectangleThreshold = 3 + + // MARK: Life Cycle + + init?(videoPreviewLayer: AVCaptureVideoPreviewLayer, delegate: RectangleDetectionDelegateProtocol? = nil) { + self.videoPreviewLayer = videoPreviewLayer + + if delegate != nil { + self.delegate = delegate + } + + super.init() + + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { + let error = ImageScannerControllerError.inputDevice + delegate?.captureSessionManager(self, didFailWithError: error) + return nil + } + + captureSession.beginConfiguration() + + photoOutput.isHighResolutionCaptureEnabled = true + + let videoOutput = AVCaptureVideoDataOutput() + videoOutput.alwaysDiscardsLateVideoFrames = true + + defer { + device.unlockForConfiguration() + captureSession.commitConfiguration() + } + + guard let deviceInput = try? AVCaptureDeviceInput(device: device), + captureSession.canAddInput(deviceInput), + captureSession.canAddOutput(photoOutput), + captureSession.canAddOutput(videoOutput) else { + let error = ImageScannerControllerError.inputDevice + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + + do { + try device.lockForConfiguration() + } catch { + let error = ImageScannerControllerError.inputDevice + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + + device.isSubjectAreaChangeMonitoringEnabled = true + + captureSession.addInput(deviceInput) + captureSession.addOutput(photoOutput) + captureSession.addOutput(videoOutput) + + let photoPreset = AVCaptureSession.Preset.photo + + if captureSession.canSetSessionPreset(photoPreset) { + captureSession.sessionPreset = photoPreset + + if photoOutput.isLivePhotoCaptureSupported { + photoOutput.isLivePhotoCaptureEnabled = true + } + } + + videoPreviewLayer.session = captureSession + videoPreviewLayer.videoGravity = .resizeAspectFill + + videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video_ouput_queue")) + } + + // MARK: Capture Session Life Cycle + + /// Starts the camera and detecting quadrilaterals. + internal func start() { + let authorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) + + switch authorizationStatus { + case .authorized: + DispatchQueue.main.async { + self.captureSession.startRunning() + } + isDetecting = true + case .notDetermined: + AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { _ in + DispatchQueue.main.async { [weak self] in + self?.start() + } + }) + default: + let error = ImageScannerControllerError.authorization + delegate?.captureSessionManager(self, didFailWithError: error) + } + } + + internal func stop() { + captureSession.stopRunning() + } + + internal func capturePhoto() { + guard let connection = photoOutput.connection(with: .video), connection.isEnabled, connection.isActive else { + let error = ImageScannerControllerError.capture + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + CaptureSession.current.setImageOrientation() + let photoSettings = AVCapturePhotoSettings() + photoSettings.isHighResolutionPhotoEnabled = true + photoSettings.isAutoStillImageStabilizationEnabled = true + photoOutput.capturePhoto(with: photoSettings, delegate: self) + } + + // MARK: - AVCaptureVideoDataOutputSampleBufferDelegate + + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard isDetecting == true, + let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + + let imageSize = CGSize(width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)) + + if #available(iOS 11.0, *) { + VisionRectangleDetector.rectangle(forPixelBuffer: pixelBuffer) { rectangle in + self.processRectangle(rectangle: rectangle, imageSize: imageSize) + } + } else { + let finalImage = CIImage(cvPixelBuffer: pixelBuffer) + CIRectangleDetector.rectangle(forImage: finalImage) { rectangle in + self.processRectangle(rectangle: rectangle, imageSize: imageSize) + } + } + } + + private func processRectangle(rectangle: Quadrilateral?, imageSize: CGSize) { + if let rectangle { + + self.noRectangleCount = 0 + self.rectangleFunnel + .add(rectangle, currentlyDisplayedRectangle: self.displayedRectangleResult?.rectangle) { [weak self] result, rectangle in + + guard let self else { + return + } + + let shouldAutoScan = (result == .showAndAutoScan) + self.displayRectangleResult(rectangleResult: RectangleDetectorResult(rectangle: rectangle, imageSize: imageSize)) + if shouldAutoScan, CaptureSession.current.isAutoScanEnabled, !CaptureSession.current.isEditing { + capturePhoto() + } + } + + } else { + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + self.noRectangleCount += 1 + + if self.noRectangleCount > self.noRectangleThreshold { + // Reset the currentAutoScanPassCount, so the threshold is restarted the next time a rectangle is found + self.rectangleFunnel.currentAutoScanPassCount = 0 + + // Remove the currently displayed rectangle as no rectangles are being found anymore + self.displayedRectangleResult = nil + self.delegate?.captureSessionManager(self, didDetectQuad: nil, imageSize) + } + } + return + + } + } + + @discardableResult private func displayRectangleResult(rectangleResult: RectangleDetectorResult) -> Quadrilateral { + displayedRectangleResult = rectangleResult + + let quad = rectangleResult.rectangle.toCartesian(withHeight: rectangleResult.imageSize.height) + + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + + self.delegate?.captureSessionManager(self, didDetectQuad: quad, rectangleResult.imageSize) + } + + return quad + } + +} + +extension CaptureSessionManager: AVCapturePhotoCaptureDelegate { + + // swiftlint:disable function_parameter_count + func photoOutput(_ captureOutput: AVCapturePhotoOutput, + didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, + previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?, + resolvedSettings: AVCaptureResolvedPhotoSettings, + bracketSettings: AVCaptureBracketedStillImageSettings?, + error: Error? + ) { + if let error { + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + + isDetecting = false + rectangleFunnel.currentAutoScanPassCount = 0 + delegate?.didStartCapturingPicture(for: self) + + if let sampleBuffer = photoSampleBuffer, + let imageData = AVCapturePhotoOutput.jpegPhotoDataRepresentation( + forJPEGSampleBuffer: sampleBuffer, + previewPhotoSampleBuffer: nil + ) { + completeImageCapture(with: imageData) + } else { + let error = ImageScannerControllerError.capture + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + + } + + @available(iOS 11.0, *) + func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { + if let error { + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + + isDetecting = false + rectangleFunnel.currentAutoScanPassCount = 0 + delegate?.didStartCapturingPicture(for: self) + + if let imageData = photo.fileDataRepresentation() { + completeImageCapture(with: imageData) + } else { + let error = ImageScannerControllerError.capture + delegate?.captureSessionManager(self, didFailWithError: error) + return + } + } + + /// Completes the image capture by processing the image, and passing it to the delegate object. + /// This function is necessary because the capture functions for iOS 10 and 11 are decoupled. + private func completeImageCapture(with imageData: Data) { + DispatchQueue.global(qos: .background).async { [weak self] in + CaptureSession.current.isEditing = true + guard let image = UIImage(data: imageData) else { + let error = ImageScannerControllerError.capture + DispatchQueue.main.async { + guard let self else { + return + } + self.delegate?.captureSessionManager(self, didFailWithError: error) + } + return + } + + var angle: CGFloat = 0.0 + + switch image.imageOrientation { + case .right: + angle = CGFloat.pi / 2 + case .up: + angle = CGFloat.pi + default: + break + } + + var quad: Quadrilateral? + if let displayedRectangleResult = self?.displayedRectangleResult { + quad = self?.displayRectangleResult(rectangleResult: displayedRectangleResult) + quad = quad?.scale(displayedRectangleResult.imageSize, image.size, withRotationAngle: angle) + } + + DispatchQueue.main.async { + guard let self else { + return + } + self.delegate?.captureSessionManager(self, didCapturePicture: image, withQuad: quad) + } + } + } +} + +/// Data structure representing the result of the detection of a quadrilateral. +private struct RectangleDetectorResult { + + /// The detected quadrilateral. + let rectangle: Quadrilateral + + /// The size of the image the quadrilateral was detected on. + let imageSize: CGSize + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/FocusRectangleView.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/FocusRectangleView.swift new file mode 100644 index 0000000..ed58c59 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/FocusRectangleView.swift @@ -0,0 +1,46 @@ +// +// FocusRectangleView.swift +// WeScan +// +// Created by Julian Schiavo on 16/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import UIKit + +/// A yellow rectangle used to display the last 'tap to focus' point +final class FocusRectangleView: UIView { + convenience init(touchPoint: CGPoint) { + let originalSize: CGFloat = 200 + let finalSize: CGFloat = 80 + + // Here, we create the frame to be the `originalSize`, with it's center being the `touchPoint`. + self.init( + frame: CGRect( + x: touchPoint.x - (originalSize / 2), + y: touchPoint.y - (originalSize / 2), + width: originalSize, + height: originalSize + ) + ) + + backgroundColor = .clear + layer.borderWidth = 2.0 + layer.cornerRadius = 6.0 + layer.borderColor = UIColor.yellow.cgColor + + // Here, we animate the rectangle from the `originalSize` to the `finalSize` by calculating the difference. + UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseOut, animations: { + self.frame.origin.x += (originalSize - finalSize) / 2 + self.frame.origin.y += (originalSize - finalSize) / 2 + + self.frame.size.width -= (originalSize - finalSize) + self.frame.size.height -= (originalSize - finalSize) + }) + } + + public func setBorder(color: CGColor) { + layer.borderColor = color + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/RectangleFeaturesFunnel.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/RectangleFeaturesFunnel.swift new file mode 100644 index 0000000..66b8565 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/RectangleFeaturesFunnel.swift @@ -0,0 +1,190 @@ +// +// RectangleFeaturesFunnel.swift +// WeScan +// +// Created by Boris Emorine on 2/9/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// +// swiftlint:disable line_length + +import AVFoundation +import Foundation + +enum AddResult { + case showAndAutoScan + case showOnly +} + +/// `RectangleFeaturesFunnel` is used to improve the confidence of the detected rectangles. +/// Feed rectangles to a `RectangleFeaturesFunnel` instance, and it will call the completion block with a rectangle whose confidence is high enough to be displayed. +final class RectangleFeaturesFunnel { + + /// `RectangleMatch` is a class used to assign matching scores to rectangles. + private final class RectangleMatch: NSObject { + /// The rectangle feature object associated to this `RectangleMatch` instance. + let rectangleFeature: Quadrilateral + + /// The score to indicate how strongly the rectangle of this instance matches other recently added rectangles. + /// A higher score indicates that many recently added rectangles are very close to the rectangle of this instance. + var matchingScore = 0 + + init(rectangleFeature: Quadrilateral) { + self.rectangleFeature = rectangleFeature + } + + override var description: String { + return "Matching score: \(matchingScore) - Rectangle: \(rectangleFeature)" + } + + /// Whether the rectangle of this instance is within the distance of the given rectangle. + /// + /// - Parameters: + /// - rectangle: The rectangle to compare the rectangle of this instance with. + /// - threshold: The distance used to determinate if the rectangles match in pixels. + /// - Returns: True if both rectangles are within the given distance of each other. + func matches(_ rectangle: Quadrilateral, withThreshold threshold: CGFloat) -> Bool { + return rectangleFeature.isWithin(threshold, ofRectangleFeature: rectangle) + } + } + + /// The queue of last added rectangles. The first rectangle is oldest one, and the last rectangle is the most recently added one. + private var rectangles = [RectangleMatch]() + + /// The maximum number of rectangles to compare newly added rectangles with. Determines the maximum size of `rectangles`. Increasing this value will impact performance. + let maxNumberOfRectangles = 8 + + /// The minimum number of rectangles needed to start making comparaisons and determining which rectangle to display. This value should always be inferior than `maxNumberOfRectangles`. + /// A higher value will delay the first time a rectangle is displayed. + let minNumberOfRectangles = 3 + + /// The value in pixels used to determine if two rectangle match or not. A higher value will prevent displayed rectangles to be refreshed. On the opposite, a smaller value will make new rectangles be displayed constantly. + let matchingThreshold: CGFloat = 40.0 + + /// The minumum number of matching rectangles (within the `rectangle` queue), to be confident enough to display a rectangle. + let minNumberOfMatches = 3 + + /// The number of similar rectangles that need to be found to auto scan. + let autoScanThreshold = 35 + + /// The number of times the rectangle has passed the threshold to be auto-scanned + var currentAutoScanPassCount = 0 + + /// The value in pixels used to determine if a rectangle is accurate enough to be auto scanned. + /// A higher value means the auto scan is quicker, but the rectangle will be less accurate. On the other hand, the lower the value, the longer it'll take for the auto scan, but it'll be way more accurate + var autoScanMatchingThreshold: CGFloat = 6.0 + + /// Add a rectangle to the funnel, and if a new rectangle should be displayed, the completion block will be called. + /// The algorithm works the following way: + /// 1. Makes sure that the funnel has been fed enough rectangles + /// 2. Removes old rectangles if needed + /// 3. Compares all of the recently added rectangles to find out which one match each other + /// 4. Within all of the recently added rectangles, finds the "best" one (@see `bestRectangle(withCurrentlyDisplayedRectangle:)`) + /// 5. If the best rectangle is different than the currently displayed rectangle, informs the listener that a new rectangle should be displayed + /// 5a. The currentAutoScanPassCount is incremented every time a new rectangle is displayed. If it passes the autoScanThreshold, we tell the listener to scan the document. + /// - Parameters: + /// - rectangleFeature: The rectangle to feed to the funnel. + /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. + /// - completion: The completion block called when a new rectangle should be displayed. + func add(_ rectangleFeature: Quadrilateral, currentlyDisplayedRectangle currentRectangle: Quadrilateral?, completion: (AddResult, Quadrilateral) -> Void) { + let rectangleMatch = RectangleMatch(rectangleFeature: rectangleFeature) + rectangles.append(rectangleMatch) + + guard rectangles.count >= minNumberOfRectangles else { + return + } + + if rectangles.count > maxNumberOfRectangles { + rectangles.removeFirst() + } + + updateRectangleMatches() + + guard let bestRectangle = bestRectangle(withCurrentlyDisplayedRectangle: currentRectangle) else { + return + } + + if let previousRectangle = currentRectangle, + bestRectangle.rectangleFeature.isWithin(autoScanMatchingThreshold, ofRectangleFeature: previousRectangle) { + currentAutoScanPassCount += 1 + if currentAutoScanPassCount > autoScanThreshold { + currentAutoScanPassCount = 0 + completion(AddResult.showAndAutoScan, bestRectangle.rectangleFeature) + } + } else { + completion(AddResult.showOnly, bestRectangle.rectangleFeature) + } + } + + /// Determines which rectangle is best to displayed. + /// The criteria used to find the best rectangle is its matching score. + /// If multiple rectangles have the same matching score, we use a tie breaker to find the best rectangle (@see breakTie(forRectangles:)). + /// Parameters: + /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. + /// Returns: The best rectangle to display given the current history. + private func bestRectangle(withCurrentlyDisplayedRectangle currentRectangle: Quadrilateral?) -> RectangleMatch? { + var bestMatch: RectangleMatch? + guard !rectangles.isEmpty else { return nil } + rectangles.reversed().forEach { rectangle in + guard let best = bestMatch else { + bestMatch = rectangle + return + } + + if rectangle.matchingScore > best.matchingScore { + bestMatch = rectangle + return + } else if rectangle.matchingScore == best.matchingScore { + guard let currentRectangle else { + return + } + + bestMatch = breakTie(between: best, rect2: rectangle, currentRectangle: currentRectangle) + } + } + + return bestMatch + } + + /// Breaks a tie between two rectangles to find out which is best to display. + /// The first passed rectangle is returned if no other criteria could be used to break the tie. + /// If the first passed rectangle (rect1) is close to the currently displayed rectangle, we pick it. + /// Otherwise if the second passed rectangle (rect2) is close to the currently displayed rectangle, we pick this one. + /// Finally, if none of the passed in rectangles are close to the currently displayed rectangle, we arbitrary pick the first one. + /// - Parameters: + /// - rect1: The first rectangle to compare. + /// - rect2: The second rectangle to compare. + /// - currentRectangle: The currently displayed rectangle. This is used to avoid displaying very close rectangles. + /// - Returns: The best rectangle to display between two rectangles with the same matching score. + private func breakTie(between rect1: RectangleMatch, rect2: RectangleMatch, currentRectangle: Quadrilateral) -> RectangleMatch { + if rect1.rectangleFeature.isWithin(matchingThreshold, ofRectangleFeature: currentRectangle) { + return rect1 + } else if rect2.rectangleFeature.isWithin(matchingThreshold, ofRectangleFeature: currentRectangle) { + return rect2 + } + + return rect1 + } + + /// Loops through all of the rectangles of the queue, and gives them a score depending on how many they match. @see `RectangleMatch.matchingScore` + private func updateRectangleMatches() { + resetMatchingScores() + guard !rectangles.isEmpty else { return } + for (i, currentRect) in rectangles.enumerated() { + for (j, rect) in rectangles.enumerated() { + if j > i && currentRect.matches(rect.rectangleFeature, withThreshold: matchingThreshold) { + currentRect.matchingScore += 1 + rect.matchingScore += 1 + } + } + } + } + + /// Resets the matching score of all of the rectangles in the queue to 0 + private func resetMatchingScores() { + guard !rectangles.isEmpty else { return } + for rectangle in rectangles { + rectangle.matchingScore = 0 + } + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/ScannerViewController.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/ScannerViewController.swift new file mode 100644 index 0000000..50c1076 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/ScannerViewController.swift @@ -0,0 +1,325 @@ +// +// ScannerViewController.swift +// WeScan +// +// Created by Boris Emorine on 2/8/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// +// swiftlint:disable line_length + +import AVFoundation +import UIKit + +/// The `ScannerViewController` offers an interface to give feedback to the user regarding quadrilaterals that are detected. It also gives the user the opportunity to capture an image with a detected rectangle. +public final class ScannerViewController: UIViewController { + + private var captureSessionManager: CaptureSessionManager? + private let videoPreviewLayer = AVCaptureVideoPreviewLayer() + + /// The view that shows the focus rectangle (when the user taps to focus, similar to the Camera app) + private var focusRectangle: FocusRectangleView! + + /// The view that draws the detected rectangles. + private let quadView = QuadrilateralView() + + /// Whether flash is enabled + private var flashEnabled = false + + /// The original bar style that was set by the host app + private var originalBarStyle: UIBarStyle? + + private lazy var shutterButton: ShutterButton = { + let button = ShutterButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(captureImage(_:)), for: .touchUpInside) + return button + }() + + private lazy var cancelButton: UIButton = { + let button = UIButton() + button.setTitle(NSLocalizedString("wescan.scanning.cancel", tableName: nil, bundle: Bundle(for: ScannerViewController.self), value: "Cancel", comment: "The cancel button"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(cancelImageScannerController), for: .touchUpInside) + return button + }() + + private lazy var autoScanButton: UIBarButtonItem = { + let title = NSLocalizedString("wescan.scanning.auto", tableName: nil, bundle: Bundle(for: ScannerViewController.self), value: "Auto", comment: "The auto button state") + let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(toggleAutoScan)) + button.tintColor = .white + + return button + }() + + private lazy var flashButton: UIBarButtonItem = { + let image = UIImage(systemName: "bolt.fill", named: "flash", in: Bundle(for: ScannerViewController.self), compatibleWith: nil) + let button = UIBarButtonItem(image: image, style: .plain, target: self, action: #selector(toggleFlash)) + button.tintColor = .white + + return button + }() + + private lazy var activityIndicator: UIActivityIndicatorView = { + let activityIndicator = UIActivityIndicatorView(style: .gray) + activityIndicator.hidesWhenStopped = true + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + return activityIndicator + }() + + // MARK: - Life Cycle + + override public func viewDidLoad() { + super.viewDidLoad() + + title = nil + view.backgroundColor = UIColor.black + + setupViews() + setupNavigationBar() + setupConstraints() + + captureSessionManager = CaptureSessionManager(videoPreviewLayer: videoPreviewLayer, delegate: self) + + originalBarStyle = navigationController?.navigationBar.barStyle + + NotificationCenter.default.addObserver(self, selector: #selector(subjectAreaDidChange), name: Notification.Name.AVCaptureDeviceSubjectAreaDidChange, object: nil) + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + setNeedsStatusBarAppearanceUpdate() + + CaptureSession.current.isEditing = false + quadView.removeQuadrilateral() + captureSessionManager?.start() + UIApplication.shared.isIdleTimerDisabled = true + + navigationController?.navigationBar.barStyle = .blackTranslucent + } + + override public func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + videoPreviewLayer.frame = view.layer.bounds + } + + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + UIApplication.shared.isIdleTimerDisabled = false + + navigationController?.navigationBar.isTranslucent = false + navigationController?.navigationBar.barStyle = originalBarStyle ?? .default + captureSessionManager?.stop() + guard let device = AVCaptureDevice.default(for: AVMediaType.video) else { return } + if device.torchMode == .on { + toggleFlash() + } + } + + // MARK: - Setups + + private func setupViews() { + view.backgroundColor = .darkGray + view.layer.addSublayer(videoPreviewLayer) + quadView.translatesAutoresizingMaskIntoConstraints = false + quadView.editable = false + view.addSubview(quadView) + view.addSubview(cancelButton) + view.addSubview(shutterButton) + view.addSubview(activityIndicator) + } + + private func setupNavigationBar() { + navigationItem.setLeftBarButton(flashButton, animated: false) + navigationItem.setRightBarButton(autoScanButton, animated: false) + + if UIImagePickerController.isFlashAvailable(for: .rear) == false { + let flashOffImage = UIImage(systemName: "bolt.slash.fill", named: "flashUnavailable", in: Bundle(for: ScannerViewController.self), compatibleWith: nil) + flashButton.image = flashOffImage + flashButton.tintColor = UIColor.lightGray + } + } + + private func setupConstraints() { + var quadViewConstraints = [NSLayoutConstraint]() + var cancelButtonConstraints = [NSLayoutConstraint]() + var shutterButtonConstraints = [NSLayoutConstraint]() + var activityIndicatorConstraints = [NSLayoutConstraint]() + + quadViewConstraints = [ + quadView.topAnchor.constraint(equalTo: view.topAnchor), + view.bottomAnchor.constraint(equalTo: quadView.bottomAnchor), + view.trailingAnchor.constraint(equalTo: quadView.trailingAnchor), + quadView.leadingAnchor.constraint(equalTo: view.leadingAnchor) + ] + + shutterButtonConstraints = [ + shutterButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + shutterButton.widthAnchor.constraint(equalToConstant: 65.0), + shutterButton.heightAnchor.constraint(equalToConstant: 65.0) + ] + + activityIndicatorConstraints = [ + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ] + + if #available(iOS 11.0, *) { + cancelButtonConstraints = [ + cancelButton.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor, constant: 24.0), + view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: (65.0 / 2) - 10.0) + ] + + let shutterButtonBottomConstraint = view.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: shutterButton.bottomAnchor, constant: 8.0) + shutterButtonConstraints.append(shutterButtonBottomConstraint) + } else { + cancelButtonConstraints = [ + cancelButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 24.0), + view.bottomAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: (65.0 / 2) - 10.0) + ] + + let shutterButtonBottomConstraint = view.bottomAnchor.constraint(equalTo: shutterButton.bottomAnchor, constant: 8.0) + shutterButtonConstraints.append(shutterButtonBottomConstraint) + } + + NSLayoutConstraint.activate(quadViewConstraints + cancelButtonConstraints + shutterButtonConstraints + activityIndicatorConstraints) + } + + // MARK: - Tap to Focus + + /// Called when the AVCaptureDevice detects that the subject area has changed significantly. When it's called, we reset the focus so the camera is no longer out of focus. + @objc private func subjectAreaDidChange() { + /// Reset the focus and exposure back to automatic + do { + try CaptureSession.current.resetFocusToAuto() + } catch { + let error = ImageScannerControllerError.inputDevice + guard let captureSessionManager else { return } + captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error) + return + } + + /// Remove the focus rectangle if one exists + CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: true) + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + guard let touch = touches.first else { return } + let touchPoint = touch.location(in: view) + let convertedTouchPoint: CGPoint = videoPreviewLayer.captureDevicePointConverted(fromLayerPoint: touchPoint) + + CaptureSession.current.removeFocusRectangleIfNeeded(focusRectangle, animated: false) + + focusRectangle = FocusRectangleView(touchPoint: touchPoint) + view.addSubview(focusRectangle) + + do { + try CaptureSession.current.setFocusPointToTapPoint(convertedTouchPoint) + } catch { + let error = ImageScannerControllerError.inputDevice + guard let captureSessionManager else { return } + captureSessionManager.delegate?.captureSessionManager(captureSessionManager, didFailWithError: error) + return + } + } + + // MARK: - Actions + + @objc private func captureImage(_ sender: UIButton) { + (navigationController as? ImageScannerController)?.flashToBlack() + shutterButton.isUserInteractionEnabled = false + captureSessionManager?.capturePhoto() + } + + @objc private func toggleAutoScan() { + if CaptureSession.current.isAutoScanEnabled { + CaptureSession.current.isAutoScanEnabled = false + autoScanButton.title = NSLocalizedString("wescan.scanning.manual", tableName: nil, bundle: Bundle(for: ScannerViewController.self), value: "Manual", comment: "The manual button state") + } else { + CaptureSession.current.isAutoScanEnabled = true + autoScanButton.title = NSLocalizedString("wescan.scanning.auto", tableName: nil, bundle: Bundle(for: ScannerViewController.self), value: "Auto", comment: "The auto button state") + } + } + + @objc private func toggleFlash() { + let state = CaptureSession.current.toggleFlash() + + let flashImage = UIImage(systemName: "bolt.fill", named: "flash", in: Bundle(for: ScannerViewController.self), compatibleWith: nil) + let flashOffImage = UIImage(systemName: "bolt.slash.fill", named: "flashUnavailable", in: Bundle(for: ScannerViewController.self), compatibleWith: nil) + + switch state { + case .on: + flashEnabled = true + flashButton.image = flashImage + flashButton.tintColor = .yellow + case .off: + flashEnabled = false + flashButton.image = flashImage + flashButton.tintColor = .white + case .unknown, .unavailable: + flashEnabled = false + flashButton.image = flashOffImage + flashButton.tintColor = UIColor.lightGray + } + } + + @objc private func cancelImageScannerController() { + guard let imageScannerController = navigationController as? ImageScannerController else { return } + imageScannerController.imageScannerDelegate?.imageScannerControllerDidCancel(imageScannerController) + } + +} + +extension ScannerViewController: RectangleDetectionDelegateProtocol { + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didFailWithError error: Error) { + + activityIndicator.stopAnimating() + shutterButton.isUserInteractionEnabled = true + + guard let imageScannerController = navigationController as? ImageScannerController else { return } + imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFailWithError: error) + } + + func didStartCapturingPicture(for captureSessionManager: CaptureSessionManager) { + activityIndicator.startAnimating() + captureSessionManager.stop() + shutterButton.isUserInteractionEnabled = false + } + + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didCapturePicture picture: UIImage, withQuad quad: Quadrilateral?) { + activityIndicator.stopAnimating() + + let editVC = EditScanViewController(image: picture, quad: quad) + navigationController?.pushViewController(editVC, animated: false) + + shutterButton.isUserInteractionEnabled = true + } + + func captureSessionManager(_ captureSessionManager: CaptureSessionManager, didDetectQuad quad: Quadrilateral?, _ imageSize: CGSize) { + guard let quad else { + // If no quad has been detected, we remove the currently displayed on on the quadView. + quadView.removeQuadrilateral() + return + } + + let portraitImageSize = CGSize(width: imageSize.height, height: imageSize.width) + + let scaleTransform = CGAffineTransform.scaleTransform(forSize: portraitImageSize, aspectFillInSize: quadView.bounds.size) + let scaledImageSize = imageSize.applying(scaleTransform) + + let rotationTransform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0) + + let imageBounds = CGRect(origin: .zero, size: scaledImageSize).applying(rotationTransform) + + let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: imageBounds, toCenterOfRect: quadView.bounds) + + let transforms = [scaleTransform, rotationTransform, translationTransform] + + let transformedQuad = quad.applyTransforms(transforms) + + quadView.drawQuadrilateral(quad: transformedQuad, animated: true) + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Scan/ShutterButton.swift b/ios/WeScan-3.0.0/Sources/WeScan/Scan/ShutterButton.swift new file mode 100644 index 0000000..a77abf7 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Scan/ShutterButton.swift @@ -0,0 +1,106 @@ +// +// ShutterButton.swift +// WeScan +// +// Created by Boris Emorine on 2/26/18. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import UIKit + +/// A simple button used for the shutter. +final class ShutterButton: UIControl { + + private let outerRingLayer = CAShapeLayer() + private let innerCircleLayer = CAShapeLayer() + + private let outerRingRatio: CGFloat = 0.80 + private let innerRingRatio: CGFloat = 0.75 + + private let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) + + override var isHighlighted: Bool { + didSet { + if oldValue != isHighlighted { + animateInnerCircleLayer(forHighlightedState: isHighlighted) + } + } + } + + // MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: frame) + layer.addSublayer(outerRingLayer) + layer.addSublayer(innerCircleLayer) + backgroundColor = .clear + isAccessibilityElement = true + accessibilityTraits = UIAccessibilityTraits.button + impactFeedbackGenerator.prepare() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Drawing + + override func draw(_ rect: CGRect) { + super.draw(rect) + + outerRingLayer.frame = rect + outerRingLayer.path = pathForOuterRing(inRect: rect).cgPath + outerRingLayer.fillColor = UIColor.white.cgColor + outerRingLayer.rasterizationScale = UIScreen.main.scale + outerRingLayer.shouldRasterize = true + + innerCircleLayer.frame = rect + innerCircleLayer.path = pathForInnerCircle(inRect: rect).cgPath + innerCircleLayer.fillColor = UIColor.white.cgColor + innerCircleLayer.rasterizationScale = UIScreen.main.scale + innerCircleLayer.shouldRasterize = true + } + + // MARK: - Animation + + private func animateInnerCircleLayer(forHighlightedState isHighlighted: Bool) { + let animation = CAKeyframeAnimation(keyPath: "transform") + var values = [ + CATransform3DMakeScale(1.0, 1.0, 1.0), + CATransform3DMakeScale(0.9, 0.9, 0.9), + CATransform3DMakeScale(0.93, 0.93, 0.93), + CATransform3DMakeScale(0.9, 0.9, 0.9) + ] + if isHighlighted == false { + values = [CATransform3DMakeScale(0.9, 0.9, 0.9), CATransform3DMakeScale(1.0, 1.0, 1.0)] + } + animation.values = values + animation.isRemovedOnCompletion = false + animation.fillMode = CAMediaTimingFillMode.forwards + animation.duration = isHighlighted ? 0.35 : 0.10 + + innerCircleLayer.add(animation, forKey: "transform") + impactFeedbackGenerator.impactOccurred() + } + + // MARK: - Paths + + private func pathForOuterRing(inRect rect: CGRect) -> UIBezierPath { + let path = UIBezierPath(ovalIn: rect) + + let innerRect = rect.scaleAndCenter(withRatio: outerRingRatio) + let innerPath = UIBezierPath(ovalIn: innerRect).reversing() + + path.append(innerPath) + + return path + } + + private func pathForInnerCircle(inRect rect: CGRect) -> UIBezierPath { + let rect = rect.scaleAndCenter(withRatio: innerRingRatio) + let path = UIBezierPath(ovalIn: rect) + + return path + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Flash.swift b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Flash.swift new file mode 100644 index 0000000..9c44841 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Flash.swift @@ -0,0 +1,45 @@ +// +// CaptureSession+Flash.swift +// WeScan +// +// Created by Julian Schiavo on 28/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation + +/// Extension to CaptureSession to manage the device flashlight +extension CaptureSession { + /// The possible states that the current device's flashlight can be in + enum FlashState { + case on + case off + case unavailable + case unknown + } + + /// Toggles the current device's flashlight on or off. + func toggleFlash() -> FlashState { + guard let device, device.isTorchAvailable else { return .unavailable } + + do { + try device.lockForConfiguration() + } catch { + return .unknown + } + + defer { + device.unlockForConfiguration() + } + + if device.torchMode == .on { + device.torchMode = .off + return .off + } else if device.torchMode == .off { + device.torchMode = .on + return .on + } + + return .unknown + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Focus.swift b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Focus.swift new file mode 100644 index 0000000..6e878b3 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Focus.swift @@ -0,0 +1,73 @@ +// +// CaptureSession+Focus.swift +// WeScan +// +// Created by Julian Schiavo on 28/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import Foundation +import UIKit + +/// Extension to CaptureSession that controls auto focus +extension CaptureSession { + /// Sets the camera's exposure and focus point to the given point + func setFocusPointToTapPoint(_ tapPoint: CGPoint) throws { + guard let device else { + let error = ImageScannerControllerError.inputDevice + throw error + } + + try device.lockForConfiguration() + + defer { + device.unlockForConfiguration() + } + + if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.autoFocus) { + device.focusPointOfInterest = tapPoint + device.focusMode = .autoFocus + } + + if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) { + device.exposurePointOfInterest = tapPoint + device.exposureMode = .continuousAutoExposure + } + } + + /// Resets the camera's exposure and focus point to automatic + func resetFocusToAuto() throws { + guard let device else { + let error = ImageScannerControllerError.inputDevice + throw error + } + + try device.lockForConfiguration() + + defer { + device.unlockForConfiguration() + } + + if device.isFocusPointOfInterestSupported && device.isFocusModeSupported(.continuousAutoFocus) { + device.focusMode = .continuousAutoFocus + } + + if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) { + device.exposureMode = .continuousAutoExposure + } + } + + /// Removes an existing focus rectangle if one exists, optionally animating the exit + func removeFocusRectangleIfNeeded(_ focusRectangle: FocusRectangleView?, animated: Bool) { + guard let focusRectangle else { return } + if animated { + UIView.animate(withDuration: 0.3, delay: 1.0, animations: { + focusRectangle.alpha = 0.0 + }, completion: { _ in + focusRectangle.removeFromSuperview() + }) + } else { + focusRectangle.removeFromSuperview() + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Orientation.swift b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Orientation.swift new file mode 100644 index 0000000..2d66df6 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession+Orientation.swift @@ -0,0 +1,60 @@ +// +// CaptureSession+Orientation.swift +// WeScan +// +// Created by Julian Schiavo on 23/11/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import CoreMotion +import Foundation +import UIKit + +/// Extension to CaptureSession with support for automatically detecting the current orientation via CoreMotion +/// Which works even if the user has enabled portrait lock. +extension CaptureSession { + /// Detect the current orientation of the device with CoreMotion and use it to set the `editImageOrientation`. + func setImageOrientation() { + let motion = CMMotionManager() + + /// This value should be 0.2, but since we only need one cycle (and stop updates immediately), + /// we set it low to get the orientation immediately + motion.accelerometerUpdateInterval = 0.01 + + guard motion.isAccelerometerAvailable else { return } + + motion.startAccelerometerUpdates(to: OperationQueue()) { data, error in + guard let data, error == nil else { return } + + /// The minimum amount of sensitivity for the landscape orientations + /// This is to prevent the landscape orientation being incorrectly used + /// Higher = easier for landscape to be detected, lower = easier for portrait to be detected + let motionThreshold = 0.35 + + if data.acceleration.x >= motionThreshold { + self.editImageOrientation = .left + } else if data.acceleration.x <= -motionThreshold { + self.editImageOrientation = .right + } else { + /// This means the device is either in the 'up' or 'down' orientation, BUT, + /// it's very rare for someone to be using their phone upside down, so we use 'up' all the time + /// Which prevents accidentally making the document be scanned upside down + self.editImageOrientation = .up + } + + motion.stopAccelerometerUpdates() + + // If the device is reporting a specific landscape orientation, we'll use it over the accelerometer's update. + // We don't use this to check for "portrait" because only the accelerometer works when portrait lock is enabled. + // For some reason, the left/right orientations are incorrect (flipped) :/ + switch UIDevice.current.orientation { + case .landscapeLeft: + self.editImageOrientation = .right + case .landscapeRight: + self.editImageOrientation = .left + default: + break + } + } + } +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession.swift b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession.swift new file mode 100644 index 0000000..d08ee10 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/Session/CaptureSession.swift @@ -0,0 +1,37 @@ +// +// CaptureSession.swift +// WeScan +// +// Created by Julian Schiavo on 23/9/2018. +// Copyright © 2018 WeTransfer. All rights reserved. +// + +import AVFoundation +import Foundation + +/// A class containing global variables and settings for this capture session +final class CaptureSession { + + static let current = CaptureSession() + + /// The AVCaptureDevice used for the flash and focus setting + var device: CaptureDevice? + + /// Whether the user is past the scanning screen or not (needed to disable auto scan on other screens) + var isEditing: Bool + + /// The status of auto scan. Auto scan tries to automatically scan a detected rectangle if it has a high enough accuracy. + var isAutoScanEnabled: Bool + + /// The orientation of the captured image + var editImageOrientation: CGImagePropertyOrientation + + private init(isAutoScanEnabled: Bool = true, editImageOrientation: CGImagePropertyOrientation = .up) { + self.device = AVCaptureDevice.default(for: .video) + + self.isEditing = false + self.isAutoScanEnabled = isAutoScanEnabled + self.editImageOrientation = editImageOrientation + } + +} diff --git a/ios/WeScan-3.0.0/Sources/WeScan/ja.lproj/Localizable.strings b/ios/WeScan-3.0.0/Sources/WeScan/ja.lproj/Localizable.strings new file mode 100644 index 0000000..eb865b2 --- /dev/null +++ b/ios/WeScan-3.0.0/Sources/WeScan/ja.lproj/Localizable.strings @@ -0,0 +1,22 @@ +/* + localizable.strings + WeScanSampleProject + + Created by Boris Emorine on 2/27/18. + Copyright © 2018 WeTransfer. All rights reserved. +*/ + +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"wescan.edit.button.next" = "次"; + +/* The title on the navigation bar of the Edit screen. */ +"wescan.edit.title" = "スキャン編集"; + +/* The title on the navigation bar of the Review screen. */ +"wescan.review.title" = "レビュー"; + +/* The button titles on the Scanning screen. */ +"wescan.scanning.cancel" = "キャンセル"; +"wescan.scanning.auto" = "自動"; +"wescan.scanning.manual" = "マニュアル"; + diff --git a/ios/WeScan-3.0.0/WeScan.podspec b/ios/WeScan-3.0.0/WeScan.podspec new file mode 100644 index 0000000..69ea3e0 --- /dev/null +++ b/ios/WeScan-3.0.0/WeScan.podspec @@ -0,0 +1,20 @@ +Pod::Spec.new do |spec| + spec.name = 'WeScan' + spec.version = '3.0.0' + spec.summary = 'Document Scanning Made Easy for iOS' + spec.description = 'WeScan makes it easy to add scanning functionalities to your iOS app! It\'s modelled after UIImagePickerController, which makes it a breeze to use.' + + spec.homepage = 'https://github.com/WeTransfer/WeScan' + spec.license = { :type => 'MIT', :file => 'LICENSE' } + spec.authors = { + 'Boris Emorine' => 'boris@wetransfer.com', + 'Antoine van der Lee' => 'antoine@wetransfer.com' + } + spec.source = { :git => 'https://github.com/WeTransfer/WeScan.git', :tag => "#{spec.version}" } + spec.social_media_url = 'https://twitter.com/WeTransfer' + + spec.swift_version = '5.0' + spec.ios.deployment_target = '13.0' + spec.source_files = 'Sources/WeScan/**/*.{h,m,swift}' + spec.resources = 'Sources/WeScan/**/*.{strings,png}' +end \ No newline at end of file diff --git a/ios/cunning_document_scanner.podspec b/ios/cunning_document_scanner.podspec index 1602abc..bdef5da 100644 --- a/ios/cunning_document_scanner.podspec +++ b/ios/cunning_document_scanner.podspec @@ -12,9 +12,11 @@ A new flutter plugin project. s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Cunning GmbH' => 'marcel@cunning.biz' } + s.resources = 'Assets/**/*' s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' + s.dependency 'WeScan' s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. diff --git a/lib/cunning_document_scanner.dart b/lib/cunning_document_scanner.dart index e2a4ca5..ba5876c 100644 --- a/lib/cunning_document_scanner.dart +++ b/lib/cunning_document_scanner.dart @@ -28,8 +28,19 @@ enum AndroidScannerMode { final int value; } +class IOSScannerOptions { + const IOSScannerOptions({ + this.isGalleryImportAllowed = false, + this.isAutoScanEnabled = true, + }); + + final bool isGalleryImportAllowed; + final bool isAutoScanEnabled; +} + class CunningDocumentScanner { static const _defaultAndroidOptions = AndroidScannerOptions(); + static const _defaultIOSOptions = IOSScannerOptions(); static const MethodChannel _channel = MethodChannel('cunning_document_scanner'); @@ -37,6 +48,7 @@ class CunningDocumentScanner { /// Call this to start get Picture workflow. static Future?> getPictures({ AndroidScannerOptions androidOptions = _defaultAndroidOptions, + IOSScannerOptions iOSOptions = _defaultIOSOptions, }) async { Map statuses = await [ Permission.camera, @@ -54,7 +66,10 @@ class CunningDocumentScanner { 'scannerMode': androidOptions.scannerMode.value, }); } else if (Platform.isIOS) { - pictures = await _channel.invokeMethod('getPictures', {}); + pictures = await _channel.invokeMethod('getPictures', { + 'isGalleryImportAllowed': iOSOptions.isGalleryImportAllowed, + 'isAutoScanEnabled': iOSOptions.isAutoScanEnabled, + }); } return pictures?.map((e) => e as String).toList(); } From c4b45f55fa4e49a72cd6b7f947db01cdee55407c Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 18 Apr 2024 11:24:34 +0200 Subject: [PATCH 3/8] more ios options --- .../DocumentScannerStoryboard.storyboard | 1 + .../ScanCameraViewController.swift | 36 +++++++++++++++---- .../SwiftCunningDocumentScannerPlugin.swift | 6 +++- lib/cunning_document_scanner.dart | 6 ++++ 4 files changed, 41 insertions(+), 8 deletions(-) rename ios/{ => Classes}/Base.lproj/DocumentScannerStoryboard.storyboard (99%) diff --git a/ios/Base.lproj/DocumentScannerStoryboard.storyboard b/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard similarity index 99% rename from ios/Base.lproj/DocumentScannerStoryboard.storyboard rename to ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard index f3e06d6..de932b5 100644 --- a/ios/Base.lproj/DocumentScannerStoryboard.storyboard +++ b/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard @@ -130,6 +130,7 @@ + diff --git a/ios/Classes/Controllers/ScanCameraViewController.swift b/ios/Classes/Controllers/ScanCameraViewController.swift index dbd5936..88b5d82 100644 --- a/ios/Classes/Controllers/ScanCameraViewController.swift +++ b/ios/Classes/Controllers/ScanCameraViewController.swift @@ -1,9 +1,8 @@ // -// NewCameraViewController.swift -// WeScanSampleProject +// ScanCameraViewController.swift +// cunning_document_scanner // -// Created by Chawatvish Worrapoj on 7/1/2020 -// Copyright © 2020 WeTransfer. All rights reserved. +// Created by Romain Boucher on 18/04/2024. // import UIKit @@ -15,9 +14,12 @@ final class ScanCameraViewController: UIViewController { @IBOutlet private weak var bottomView: UIView! @IBOutlet private weak var galleryButton: UIButton! @IBOutlet private weak var autoModeButton: UIButton! + @IBOutlet private weak var flashButton: UIBarButtonItem! var controller: CameraScannerViewController! var isGalleryImportAllowed: Bool = false var isAutoScanEnabled: Bool = true + var isAutoScanAllowed: Bool = true + var isFlashAllowed: Bool = true var result: FlutterResult!; override func viewDidLoad() { @@ -25,10 +27,12 @@ final class ScanCameraViewController: UIViewController { setupView() } - public func setParams(result: @escaping FlutterResult, isGalleryImportAllowed: Bool = false, isAutoScanEnabled: Bool = true) { + public func setParams(result: @escaping FlutterResult, isGalleryImportAllowed: Bool, isAutoScanEnabled: Bool, isAutoScanAllowed: Bool, isFlashAllowed: Bool) { self.result = result self.isGalleryImportAllowed = isGalleryImportAllowed self.isAutoScanEnabled = isAutoScanEnabled + self.isAutoScanAllowed = isAutoScanAllowed + self.isFlashAllowed = isFlashAllowed } private func setupView() { @@ -42,19 +46,37 @@ final class ScanCameraViewController: UIViewController { self.addChild(controller) controller.didMove(toParent: self) controller.delegate = self - controller.isAutoScanEnabled = isAutoScanEnabled; - galleryButton.isHidden = !isGalleryImportAllowed + if(!isAutoScanAllowed){ + controller.isAutoScanEnabled = false; + }else{ + controller.isAutoScanEnabled = isAutoScanEnabled; + } setAutoModeButtonView() + setGalleryButtonView() + setFlashButtonView() } private func setAutoModeButtonView() { + autoModeButton.isHidden = !isAutoScanAllowed if(controller.isAutoScanEnabled) { autoModeButton.setTitle("Auto", for: .normal) } else { autoModeButton.setTitle("Manual", for: .normal) } } + + private func setGalleryButtonView() { + galleryButton.isHidden = !isGalleryImportAllowed + } + + private func setFlashButtonView() { + if #available(iOS 16.0, *) { + flashButton.isHidden = !isFlashAllowed + }else{ + flashButton.isEnabled = !isFlashAllowed + } + } @IBAction func flashTapped(_ sender: UIButton) { controller.toggleFlash() diff --git a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift index 95aedc4..b50208e 100644 --- a/ios/Classes/SwiftCunningDocumentScannerPlugin.swift +++ b/ios/Classes/SwiftCunningDocumentScannerPlugin.swift @@ -15,6 +15,8 @@ public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin { let args = call.arguments as! Dictionary let isGalleryImportAllowed = args["isGalleryImportAllowed"] as? Bool ?? false let isAutoScanEnabled = args["isAutoScanEnabled"] as? Bool ?? false + let isAutoScanAllowed = args["isAutoScanAllowed"] as? Bool ?? false + let isFlashAllowed = args["isFlashAllowed"] as? Bool ?? false if (call.method == "getPictures") { @@ -28,7 +30,9 @@ public class SwiftCunningDocumentScannerPlugin: NSObject, FlutterPlugin { (controller.viewControllers.first as? ScanCameraViewController)?.setParams( result: result, isGalleryImportAllowed: isGalleryImportAllowed, - isAutoScanEnabled: isAutoScanEnabled + isAutoScanEnabled: isAutoScanEnabled, + isAutoScanAllowed: isAutoScanAllowed, + isFlashAllowed: isFlashAllowed ) viewController.present(controller, animated: true, completion: nil) diff --git a/lib/cunning_document_scanner.dart b/lib/cunning_document_scanner.dart index ba5876c..d01ee3c 100644 --- a/lib/cunning_document_scanner.dart +++ b/lib/cunning_document_scanner.dart @@ -31,11 +31,15 @@ enum AndroidScannerMode { class IOSScannerOptions { const IOSScannerOptions({ this.isGalleryImportAllowed = false, + this.isAutoScanAllowed = true, this.isAutoScanEnabled = true, + this.isFlashAllowed = true, }); final bool isGalleryImportAllowed; + final bool isAutoScanAllowed; final bool isAutoScanEnabled; + final bool isFlashAllowed; } class CunningDocumentScanner { @@ -68,7 +72,9 @@ class CunningDocumentScanner { } else if (Platform.isIOS) { pictures = await _channel.invokeMethod('getPictures', { 'isGalleryImportAllowed': iOSOptions.isGalleryImportAllowed, + 'isAutoScanAllowed': iOSOptions.isAutoScanAllowed, 'isAutoScanEnabled': iOSOptions.isAutoScanEnabled, + 'isFlashAllowed': iOSOptions.isFlashAllowed, }); } return pictures?.map((e) => e as String).toList(); From 423c456af0668b7882dc65bee266139943ef18cd Mon Sep 17 00:00:00 2001 From: Romain Date: Thu, 18 Apr 2024 16:01:17 +0200 Subject: [PATCH 4/8] update UI --- example/ios/Podfile.lock | 15 +- example/lib/main.dart | 3 + ios/Assets/flash_off.svg | 1 + ios/Assets/flash_on.svg | 1 + ios/Assets/gallery.svg | 1 + .../DocumentScannerStoryboard.storyboard | 158 ++++++++++-------- .../Controllers/EditImageViewController.swift | 15 ++ .../ReviewImageViewController.swift | 4 + .../ScanCameraViewController.swift | 78 +++++++-- ios/Classes/Controls/ShutterButton.swift | 103 ++++++++++++ .../SwiftCunningDocumentScannerPlugin.swift | 15 +- ios/Classes/Utils/UIColorExtension.swift | 26 +++ ios/cunning_document_scanner.podspec | 1 + lib/cunning_document_scanner.dart | 8 + 14 files changed, 344 insertions(+), 85 deletions(-) create mode 100644 ios/Assets/flash_off.svg create mode 100644 ios/Assets/flash_on.svg create mode 100644 ios/Assets/gallery.svg create mode 100644 ios/Classes/Controls/ShutterButton.swift create mode 100644 ios/Classes/Utils/UIColorExtension.swift diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 23fa47d..cb1a4aa 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,10 @@ PODS: + - CocoaLumberjack (3.8.5): + - CocoaLumberjack/Core (= 3.8.5) + - CocoaLumberjack/Core (3.8.5) - cunning_document_scanner (1.0.0): - Flutter + - SVGKit - WeScan - Flutter (1.0.0) - path_provider_foundation (0.0.1): @@ -8,6 +12,8 @@ PODS: - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter + - SVGKit (3.0.0): + - CocoaLumberjack (~> 3.0) - WeScan (3.0.0) DEPENDENCIES: @@ -17,6 +23,11 @@ DEPENDENCIES: - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - WeScan (from `.symlinks/plugins/cunning_document_scanner/ios/WeScan-3.0.0`) +SPEC REPOS: + trunk: + - CocoaLumberjack + - SVGKit + EXTERNAL SOURCES: cunning_document_scanner: :path: ".symlinks/plugins/cunning_document_scanner/ios" @@ -30,10 +41,12 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cunning_document_scanner/ios/WeScan-3.0.0" SPEC CHECKSUMS: - cunning_document_scanner: 92b962b74cb33460ccb4a27c8154a9fc9d37f24c + CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0 + cunning_document_scanner: 0a78488e382d8cc11743541cf84983d1f6512521 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 + SVGKit: 1ad7513f8c74d9652f94ed64ddecda1a23864dea WeScan: f50277f7304d146d9ebc4ebdc1b587df84dcdd8b PODFILE CHECKSUM: c1a4e03013e58f8bd901ec6ecbff4df9feb094cc diff --git a/example/lib/main.dart b/example/lib/main.dart index b562ec0..8e71df9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -56,6 +56,9 @@ class _MyAppState extends State { ), iOSOptions: const IOSScannerOptions( isGalleryImportAllowed: true, + isFlashAllowed: true, + backgroundColor: Color(0xFF333333), + tintColor: Colors.white, ), ) ?? []; diff --git a/ios/Assets/flash_off.svg b/ios/Assets/flash_off.svg new file mode 100644 index 0000000..e2d2123 --- /dev/null +++ b/ios/Assets/flash_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/Assets/flash_on.svg b/ios/Assets/flash_on.svg new file mode 100644 index 0000000..534ca95 --- /dev/null +++ b/ios/Assets/flash_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/Assets/gallery.svg b/ios/Assets/gallery.svg new file mode 100644 index 0000000..9c6b0e2 --- /dev/null +++ b/ios/Assets/gallery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard b/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard index de932b5..48be689 100644 --- a/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard +++ b/ios/Classes/Base.lproj/DocumentScannerStoryboard.storyboard @@ -2,7 +2,6 @@ - @@ -41,64 +40,74 @@ - - + - + + + + + + + - - - + + - + - + + + + + + + + + + + + + + + + - - - + + + + + + + + - + - - - - - - - - - + + + + + + + + + + + @@ -115,23 +124,20 @@ - + - - - - - - + + + - - + + @@ -148,27 +154,44 @@ - - + + + + + + + + + + + + + - - + + + - - + + + + + + + + + + + + @@ -237,6 +233,7 @@ + diff --git a/ios/Classes/Controllers/EditImageViewController.swift b/ios/Classes/Controllers/EditImageViewController.swift index 2d0a9ef..6e71f69 100644 --- a/ios/Classes/Controllers/EditImageViewController.swift +++ b/ios/Classes/Controllers/EditImageViewController.swift @@ -11,6 +11,7 @@ import WeScan class EditImageViewController : UIViewController{ @IBOutlet private weak var editImageView: UIView! @IBOutlet private weak var rotateButton: UIImageView! + @IBOutlet private weak var nextButton: UIBarButtonItem! var result: FlutterResult!; var captureImage: UIImage!; var quad: Quadrilateral? @@ -28,7 +29,8 @@ class EditImageViewController : UIViewController{ } private func setupView() { - navigationItem.title = "Edit" + navigationItem.title = NSLocalizedString("edit.title", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") + nextButton.title = NSLocalizedString("edit.button.next", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") self.view.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor editImageView.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor @@ -50,7 +52,7 @@ class EditImageViewController : UIViewController{ rotateButton.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(rotateTapped))) } - @IBAction func cropTapped(_ sender: UIButton!) { + @IBAction func nextTapped(_ sender: UIButton!) { controller.cropImage() } diff --git a/ios/Classes/Controllers/ReviewImageViewController.swift b/ios/Classes/Controllers/ReviewImageViewController.swift index 9668541..aaf7fcb 100644 --- a/ios/Classes/Controllers/ReviewImageViewController.swift +++ b/ios/Classes/Controllers/ReviewImageViewController.swift @@ -10,6 +10,7 @@ import Foundation final class ReviewImageViewController: UIViewController { @IBOutlet private weak var imageView: UIImageView! + @IBOutlet private weak var doneButton: UIBarButtonItem! var image: UIImage! var result: FlutterResult! @@ -20,8 +21,8 @@ final class ReviewImageViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - - navigationItem.title = "Review" + navigationItem.title = NSLocalizedString("review.title", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") + doneButton.title = NSLocalizedString("review.button.done", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") view.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor imageView.image = image diff --git a/ios/Classes/Controllers/ScanCameraViewController.swift b/ios/Classes/Controllers/ScanCameraViewController.swift index 534de27..22cf09a 100644 --- a/ios/Classes/Controllers/ScanCameraViewController.swift +++ b/ios/Classes/Controllers/ScanCameraViewController.swift @@ -41,7 +41,7 @@ final class ScanCameraViewController: UIViewController { } private func setupView() { - navigationItem.title = "Scan" + navigationItem.title = NSLocalizedString("scan.title", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: SwiftCunningDocumentScannerPlugin.tintColor] navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: SwiftCunningDocumentScannerPlugin.tintColor] navigationController?.navigationBar.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor @@ -80,12 +80,13 @@ final class ScanCameraViewController: UIViewController { autoModeSwitch.tintColor = SwiftCunningDocumentScannerPlugin.tintColor autoModeSwitch.isHidden = !isAutoScanAllowed autoModeSwitch.isOn = controller.isAutoScanEnabled + autoModeLabel.isHidden = !isAutoScanAllowed autoModeLabel.textColor = SwiftCunningDocumentScannerPlugin.tintColor if(controller.isAutoScanEnabled) { - autoModeLabel.text = "Auto" + autoModeLabel.text = NSLocalizedString("scanning.auto", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") } else { - autoModeLabel.text = "Manual" + autoModeLabel.text = NSLocalizedString("scanning.manual", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") } } @@ -112,6 +113,7 @@ final class ScanCameraViewController: UIViewController { } private func setBackButtonView() { + backButton.title = NSLocalizedString("scanning.cancel", bundle: Bundle(for: CunningDocumentScannerPlugin.self), comment: "Localizable") } private func setShutterButtonView() { diff --git a/ios/Classes/DocumentPhotoScanController.swift b/ios/Classes/DocumentPhotoScanController.swift deleted file mode 100644 index e1da231..0000000 --- a/ios/Classes/DocumentPhotoScanController.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// ScanPhotoViewController.swift -// edge_detection -// -// Created by Henry Leung on 3/9/2021. -// - -import WeScan -import Flutter -import Foundation - -class DocumentPhotoScanController: UIViewController, ImageScannerControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { - - var _result:FlutterResult? - - public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - picker.dismiss(animated: true) - - _result!(nil) - dismiss(animated: true) - } - - public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { - picker.dismiss(animated: true) - - guard let image = info[.originalImage] as? UIImage else { return } - let scannerVC = ImageScannerController(image: image) - scannerVC.imageScannerDelegate = self - - if #available(iOS 13.0, *) { - scannerVC.isModalInPresentation = true - scannerVC.overrideUserInterfaceStyle = .dark - scannerVC.view.backgroundColor = .black - } - present(scannerVC, animated: true) - } - - override func viewDidAppear(_ animated: Bool) { - - // Temp fix for https://github.com/WeTransfer/WeScan/issues/320 - if #available(iOS 15, *) { - let appearance = UINavigationBarAppearance() - let navigationBar = UINavigationBar() - appearance.configureWithOpaqueBackground() - appearance.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.label] - appearance.backgroundColor = .systemBackground - navigationBar.standardAppearance = appearance; - UINavigationBar.appearance().scrollEdgeAppearance = appearance - - let appearanceTB = UITabBarAppearance() - appearanceTB.configureWithOpaqueBackground() - appearanceTB.backgroundColor = .systemBackground - UITabBar.appearance().standardAppearance = appearanceTB - UITabBar.appearance().scrollEdgeAppearance = appearanceTB - } - - if self.isBeingPresented { - let imagePicker = UIImagePickerController() - imagePicker.delegate = self - imagePicker.sourceType = .photoLibrary - - present(imagePicker, animated: true) - } - } - - func imageScannerController(_ scanner: ImageScannerController, didFailWithError error: Error) { - print(error) - _result!(nil) - self.dismiss(animated: true) - } - - func imageScannerController(_ scanner: ImageScannerController, didFinishScanningWithResults results: ImageScannerResults) { - // Your ViewController is responsible for dismissing the ImageScannerController - scanner.dismiss(animated: true) - - - let filePath = saveImage(image:results.doesUserPreferEnhancedScan ? results.enhancedScan!.image : results.croppedScan.image) - var filenames: [String] = [] - if(filePath != nil){ - filenames.append(filePath!) - } - _result!(filenames) - self.dismiss(animated: true) - } - - - func imageScannerControllerDidCancel(_ scanner: ImageScannerController) { - // Your ViewController is responsible for dismissing the ImageScannerController - scanner.dismiss(animated: true) - _result!(nil) - self.dismiss(animated: true) - } - - - func saveImage(image: UIImage) -> String? { - guard let data = image.pngData() else { - return nil - } - - let tempDirPath = getDocumentsDirectory() - let currentDateTime = Date() - let df = DateFormatter() - df.dateFormat = "yyyyMMdd-HHmmss" - let formattedDate = df.string(from: currentDateTime) - let filePath = tempDirPath.appendingPathComponent(formattedDate + ".png") - - do { - let fileManager = FileManager.default - - // Check if file exists - if fileManager.fileExists(atPath: filePath.path) { - // Delete file - try fileManager.removeItem(atPath: filePath.path) - } else { - print("File does not exist") - } - - } - catch let error as NSError { - print("An error took place: \(error)") - } - - do { - try data.write(to: filePath) - return filePath.path - } catch { - print(error.localizedDescription) - return nil - } - } - - func getDocumentsDirectory() -> URL { - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - let documentsDirectory = paths[0] - return documentsDirectory - } -} diff --git a/ios/Assets/flash_off.svg b/ios/Ressources/Assets/flash_off.svg similarity index 100% rename from ios/Assets/flash_off.svg rename to ios/Ressources/Assets/flash_off.svg diff --git a/ios/Assets/flash_on.svg b/ios/Ressources/Assets/flash_on.svg similarity index 100% rename from ios/Assets/flash_on.svg rename to ios/Ressources/Assets/flash_on.svg diff --git a/ios/Ressources/Localisation/en.lproj/Localizable.strings b/ios/Ressources/Localisation/en.lproj/Localizable.strings new file mode 100644 index 0000000..dcdcf51 --- /dev/null +++ b/ios/Ressources/Localisation/en.lproj/Localizable.strings @@ -0,0 +1,17 @@ +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"edit.button.next" = "Next"; + +/* The title on the navigation bar of the Edit screen. */ +"edit.title" = "Edit"; + +/* The title on the navigation bar of the Review screen. */ +"review.title" = "Review"; +"review.button.done" = "Done"; + +/* The title on the navigation bar of the Scan screen. */ +"scan.title" = "Scan"; + +/* The button titles on the Scanning screen. */ +"scanning.cancel" = "Cancel"; +"scanning.auto" = "Auto"; +"scanning.manual" = "Manual"; diff --git a/ios/Ressources/Localisation/fr.lproj/Localizable.strings b/ios/Ressources/Localisation/fr.lproj/Localizable.strings new file mode 100644 index 0000000..6117e3b --- /dev/null +++ b/ios/Ressources/Localisation/fr.lproj/Localizable.strings @@ -0,0 +1,17 @@ +/* The "Next" button on the right side of the navigation bar on the Edit screen. */ +"edit.button.next" = "Suivant"; + +/* The title on the navigation bar of the Edit screen. */ +"edit.title" = "Modifier"; + +/* The title on the navigation bar of the Review screen. */ +"review.title" = "Aperçu"; +"review.button.done" = "Valider"; + +/* The title on the navigation bar of the Scan screen. */ +"scan.title" = "Scan"; + +/* The button titles on the Scanning screen. */ +"scanning.cancel" = "Annuler"; +"scanning.auto" = "Auto"; +"scanning.manual" = "Manuel"; diff --git a/ios/cunning_document_scanner.podspec b/ios/cunning_document_scanner.podspec index 980877f..5ea08a9 100644 --- a/ios/cunning_document_scanner.podspec +++ b/ios/cunning_document_scanner.podspec @@ -12,7 +12,7 @@ A new flutter plugin project. s.homepage = 'http://example.com' s.license = { :file => '../LICENSE' } s.author = { 'Cunning GmbH' => 'marcel@cunning.biz' } - s.resources = 'Assets/**/*' + s.resources = 'Ressources/**/*' s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' From 712537162736012966fc6f9ff5c016a2ed6f69a6 Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 19 Apr 2024 10:59:36 +0200 Subject: [PATCH 6/8] fix image orientation --- .../Controllers/EditImageViewController.swift | 5 +++-- ios/Classes/Utils/UIImageExtension.swift | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 ios/Classes/Utils/UIImageExtension.swift diff --git a/ios/Classes/Controllers/EditImageViewController.swift b/ios/Classes/Controllers/EditImageViewController.swift index 6e71f69..c28404c 100644 --- a/ios/Classes/Controllers/EditImageViewController.swift +++ b/ios/Classes/Controllers/EditImageViewController.swift @@ -34,9 +34,10 @@ class EditImageViewController : UIViewController{ self.view.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor editImageView.backgroundColor = SwiftCunningDocumentScannerPlugin.backgroundColor - + let imageForced = captureImage.forceSameOrientation() + let orientedImage = UIImage(cgImage: imageForced.cgImage!, scale: imageForced.scale, orientation: .down) controller = WeScan.EditImageViewController( - image: captureImage, + image: orientedImage, quad: quad, strokeColor: UIColor(red: (69.0 / 255.0), green: (194.0 / 255.0), blue: (177.0 / 255.0), alpha: 1.0).cgColor ) diff --git a/ios/Classes/Utils/UIImageExtension.swift b/ios/Classes/Utils/UIImageExtension.swift new file mode 100644 index 0000000..89b7d37 --- /dev/null +++ b/ios/Classes/Utils/UIImageExtension.swift @@ -0,0 +1,12 @@ +extension UIImage { + func forceSameOrientation() -> UIImage { + UIGraphicsBeginImageContext(self.size) + self.draw(in: CGRect(origin: CGPoint.zero, size: self.size)) + guard let image = UIGraphicsGetImageFromCurrentImageContext() else { + UIGraphicsEndImageContext() + return self + } + UIGraphicsEndImageContext() + return image + } +} From fcf5e573a1fb05d72c1b89ef202e7b39c4ae641f Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 19 Apr 2024 11:00:11 +0200 Subject: [PATCH 7/8] disable autoscan when imagePicker is activated --- .../Controllers/ScanCameraViewController.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ios/Classes/Controllers/ScanCameraViewController.swift b/ios/Classes/Controllers/ScanCameraViewController.swift index 22cf09a..af6d1ea 100644 --- a/ios/Classes/Controllers/ScanCameraViewController.swift +++ b/ios/Classes/Controllers/ScanCameraViewController.swift @@ -27,6 +27,8 @@ final class ScanCameraViewController: UIViewController { var flashEnabled = false; + var oldAutoScanState = false; + override func viewDidLoad() { super.viewDidLoad() setupView() @@ -145,6 +147,10 @@ final class ScanCameraViewController: UIViewController { } @IBAction func galleryTapped(_ sender: UIButton) { + //Disable autoscan when open image picker + oldAutoScanState = controller.isAutoScanEnabled; + controller.isAutoScanEnabled = false + let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary @@ -174,13 +180,16 @@ extension ScanCameraViewController: CameraScannerViewOutputDelegate { extension ScanCameraViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate{ public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { picker.dismiss(animated: true) + //Restore autoscan state + controller.isAutoScanEnabled = oldAutoScanState } public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { picker.dismiss(animated: true) + //Restore autoscan state + controller.isAutoScanEnabled = oldAutoScanState guard let image = info[.originalImage] as? UIImage else { return } - pushEditDocumentViewController(image: image, quad: nil) } } From 1068b0d95c54ed578999cc52b082e7a8762dd15f Mon Sep 17 00:00:00 2001 From: Romain Date: Fri, 19 Apr 2024 16:46:04 +0200 Subject: [PATCH 8/8] Update README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index f159a12..cd8e751 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,22 @@ platform :ios, '13.0' end end ``` +3. Add this line to your Podfile in your project: + +``` +pod 'WeScan', :path => '.symlinks/plugins/cuning_document_scanner/ios/WeScan-3.0.0' +``` + +=> like this below: + +``` +target 'Runner' do + use_frameworks! + use_modular_headers! + pod 'WeScan', :path => '.symlinks/plugins/cuning_document_scanner/ios/WeScan-3.0.0' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end +``` ## How to use ?