diff --git a/WeScan.xcodeproj/project.pbxproj b/WeScan.xcodeproj/project.pbxproj index 076abbcc..3dfe631b 100644 --- a/WeScan.xcodeproj/project.pbxproj +++ b/WeScan.xcodeproj/project.pbxproj @@ -8,6 +8,11 @@ /* Begin PBXBuildFile section */ 362967782294C23700B9FC4A /* CGImagePropertyOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362967772294C23700B9FC4A /* CGImagePropertyOrientation.swift */; }; + 382D0B0323C348A800A81619 /* CameraScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 382D0B0223C348A800A81619 /* CameraScannerViewController.swift */; }; + 383C440E23C5846B0070DE47 /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383C440D23C5846B0070DE47 /* EditImageViewController.swift */; }; + 383C441723C587B90070DE47 /* ReviewImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 383C441623C587B90070DE47 /* ReviewImageViewController.swift */; }; + 388A3BF423C46DAE00263DD1 /* NewCameraViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388A3BF323C46DAE00263DD1 /* NewCameraViewController.swift */; }; + 388A3BFD23C46FF000263DD1 /* EditImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388A3BFC23C46FF000263DD1 /* EditImageViewController.swift */; }; 74E27858215446C900361812 /* FBSnapshotTestCase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74E27850215446C200361812 /* FBSnapshotTestCase.framework */; }; 74F7D034211ACBD90046AF7E /* CIRectangleDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F7D033211ACBD90046AF7E /* CIRectangleDetectorTests.swift */; }; 74F7D036211ACBEE0046AF7E /* CaptureSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74F7D035211ACBEE0046AF7E /* CaptureSessionTests.swift */; }; @@ -187,6 +192,11 @@ /* Begin PBXFileReference section */ 274DF35922D363BC0095CE49 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; 362967772294C23700B9FC4A /* CGImagePropertyOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGImagePropertyOrientation.swift; sourceTree = ""; }; + 382D0B0223C348A800A81619 /* CameraScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraScannerViewController.swift; sourceTree = ""; }; + 383C440D23C5846B0070DE47 /* EditImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageViewController.swift; sourceTree = ""; }; + 383C441623C587B90070DE47 /* ReviewImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewImageViewController.swift; sourceTree = ""; }; + 388A3BF323C46DAE00263DD1 /* NewCameraViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewCameraViewController.swift; sourceTree = ""; }; + 388A3BFC23C46FF000263DD1 /* EditImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditImageViewController.swift; sourceTree = ""; }; 37DE11D92417FC8A0062E4E4 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 4A644E2221A73C2B00B20839 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 731898DA243A753C00EA7356 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; @@ -385,6 +395,9 @@ A1D4BCC1202C4F3800FCDDEC /* Assets.xcassets */, A1D4BCC3202C4F3800FCDDEC /* LaunchScreen.storyboard */, A1D4BCC6202C4F3800FCDDEC /* Info.plist */, + 388A3BF323C46DAE00263DD1 /* NewCameraViewController.swift */, + 383C440D23C5846B0070DE47 /* EditImageViewController.swift */, + 383C441623C587B90070DE47 /* ReviewImageViewController.swift */, ); path = WeScanSampleProject; sourceTree = ""; @@ -478,11 +491,12 @@ A1F22ECF203199E7001723AD /* Scan */ = { isa = PBXGroup; children = ( - A1D4BD0B202C504F00FCDDEC /* ScannerViewController.swift */, + 382D0B0223C348A800A81619 /* CameraScannerViewController.swift */, A1D4BD0D202C57A400FCDDEC /* CaptureSessionManager.swift */, + B9AAE88A219E6C0400205620 /* FocusRectangleView.swift */, A1F22EA2202DAA74001723AD /* RectangleFeaturesFunnel.swift */, + A1D4BD0B202C504F00FCDDEC /* ScannerViewController.swift */, A165F67D2044741B002D5ED6 /* ShutterButton.swift */, - B9AAE88A219E6C0400205620 /* FocusRectangleView.swift */, ); path = Scan; sourceTree = ""; @@ -490,6 +504,7 @@ A1F22ED020319A03001723AD /* Edit */ = { isa = PBXGroup; children = ( + 388A3BFC23C46FF000263DD1 /* EditImageViewController.swift */, A1F22ECD2031937E001723AD /* EditScanViewController.swift */, C31DD8A220C087D80072D439 /* ZoomGestureController.swift */, ); @@ -644,12 +659,12 @@ A1D4BCB6202C4F3800FCDDEC = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; + ProvisioningStyle = Manual; }; A1D4BCEC202C4F4100FCDDEC = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1020; - ProvisioningStyle = Automatic; + ProvisioningStyle = Manual; }; A1D4BCF4202C4F4100FCDDEC = { CreatedOnToolsVersion = 9.2; @@ -848,7 +863,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 383C440E23C5846B0070DE47 /* EditImageViewController.swift in Sources */, + 383C441723C587B90070DE47 /* ReviewImageViewController.swift in Sources */, A1D4BCBD202C4F3800FCDDEC /* HomeViewController.swift in Sources */, + 388A3BF423C46DAE00263DD1 /* NewCameraViewController.swift in Sources */, A1D4BCBB202C4F3800FCDDEC /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -865,8 +883,10 @@ A1F22ECE2031937E001723AD /* EditScanViewController.swift in Sources */, A1F22E9F202C8D70001723AD /* Quadrilateral.swift in Sources */, A1DF90E420358CB100841A11 /* Transformable.swift in Sources */, + 388A3BFD23C46FF000263DD1 /* EditImageViewController.swift in Sources */, B9253F3F22190B7400C5DE7C /* CGSize+Utils.swift in Sources */, A1D4BD0C202C504F00FCDDEC /* ScannerViewController.swift in Sources */, + 382D0B0323C348A800A81619 /* CameraScannerViewController.swift in Sources */, A11C5B9C2046A20C005075FE /* Error.swift in Sources */, B940E3DE21AE2A79003B3C0B /* CaptureSession+Focus.swift in Sources */, 8A1BB199249C9B45000278F2 /* UIImage+SFSymbol.swift in Sources */, @@ -1099,13 +1119,15 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7P4LQNMZS5; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WeScanSampleProject/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.WeTransfer.WeScanSampleProject; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1116,13 +1138,15 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7P4LQNMZS5; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WeScanSampleProject/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.WeTransfer.WeScanSampleProject; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; @@ -1132,11 +1156,11 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 74N8K5J2N7; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1146,6 +1170,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = WeTransfer.WeScan; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -1159,11 +1185,11 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 74N8K5J2N7; + DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1173,6 +1199,8 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = WeTransfer.WeScan; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = ""; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; diff --git a/WeScan/Common/EditScanCornerView.swift b/WeScan/Common/EditScanCornerView.swift index ae7cc381..e103fd7b 100644 --- a/WeScan/Common/EditScanCornerView.swift +++ b/WeScan/Common/EditScanCornerView.swift @@ -24,6 +24,13 @@ final class EditScanCornerView: UIView { layer.lineWidth = 1.0 return layer }() + + /// Set stroke color of coner layer + public var strokeColor: CGColor? { + didSet { + circleLayer.strokeColor = strokeColor + } + } init(frame: CGRect, position: CornerPosition) { self.position = position diff --git a/WeScan/Common/QuadrilateralView.swift b/WeScan/Common/QuadrilateralView.swift index 5241f6de..80b93b48 100644 --- a/WeScan/Common/QuadrilateralView.swift +++ b/WeScan/Common/QuadrilateralView.swift @@ -53,6 +53,17 @@ final class QuadrilateralView: UIView { layoutCornerViews(forQuad: quad) } } + + /// Set stroke color of image rect and coner. + 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) { diff --git a/WeScan/Edit/EditImageViewController.swift b/WeScan/Edit/EditImageViewController.swift new file mode 100644 index 00000000..8aab5ca4 --- /dev/null +++ b/WeScan/Edit/EditImageViewController.swift @@ -0,0 +1,204 @@ +// +// EditImageViewController.swift +// WeScan +// +// Created by Chawatvish Worrapoj on 7/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit +import AVFoundation + +/// A protocol that your delegate object will get results of EditImageViewController. +public protocol EditImageViewDelegate: class { + /// 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/WeScan/Edit/EditScanViewController.swift b/WeScan/Edit/EditScanViewController.swift index faa4c4a9..298d9ce7 100644 --- a/WeScan/Edit/EditScanViewController.swift +++ b/WeScan/Edit/EditScanViewController.swift @@ -43,7 +43,7 @@ final class EditScanViewController: UIViewController { button.tintColor = navigationController?.navigationBar.tintColor return button }() - + /// The image the quadrilateral was detected on. private let image: UIImage @@ -67,7 +67,7 @@ final class EditScanViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() setupViews() @@ -75,11 +75,11 @@ final class EditScanViewController: UIViewController { 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 + navigationItem.leftBarButtonItem = cancelButton } else { navigationItem.leftBarButtonItem = nil } - + zoomGestureController = ZoomGestureController(image: image, quadView: quadView) let touchDown = UILongPressGestureRecognizer(target: zoomGestureController, action: #selector(zoomGestureController.handle(pan:))) @@ -87,13 +87,13 @@ final class EditScanViewController: UIViewController { view.addGestureRecognizer(touchDown) } - override func viewDidLayoutSubviews() { + override public func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() adjustQuadViewConstraints() displayQuad() } - override func viewWillDisappear(_ animated: Bool) { + 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. @@ -115,7 +115,7 @@ final class EditScanViewController: UIViewController { 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) @@ -159,7 +159,7 @@ final class EditScanViewController: UIViewController { "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight), "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft), "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight) - ]) + ]) let croppedImage = UIImage.from(ciImage: filteredImage) // Enhanced Image @@ -171,7 +171,7 @@ final class EditScanViewController: UIViewController { 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)) @@ -202,5 +202,5 @@ final class EditScanViewController: UIViewController { return quad } - + } diff --git a/WeScan/Scan/CameraScannerViewController.swift b/WeScan/Scan/CameraScannerViewController.swift new file mode 100644 index 00000000..891138f1 --- /dev/null +++ b/WeScan/Scan/CameraScannerViewController.swift @@ -0,0 +1,194 @@ +// +// CameraScannerViewController.swift +// WeScan +// +// Created by Chawatvish Worrapoj on 6/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit +import AVFoundation + +/// 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: class { + 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 = 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 = 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 = 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/WeScan/Scan/FocusRectangleView.swift b/WeScan/Scan/FocusRectangleView.swift index 1146deaa..a8d83182 100644 --- a/WeScan/Scan/FocusRectangleView.swift +++ b/WeScan/Scan/FocusRectangleView.swift @@ -32,4 +32,8 @@ final class FocusRectangleView: UIView { }) } + public func setBorder(color: CGColor) { + layer.borderColor = color + } + } diff --git a/WeScanSampleProject/Base.lproj/Main.storyboard b/WeScanSampleProject/Base.lproj/Main.storyboard index 1d669170..dcdf5393 100644 --- a/WeScanSampleProject/Base.lproj/Main.storyboard +++ b/WeScanSampleProject/Base.lproj/Main.storyboard @@ -1,7 +1,9 @@ - + + - + + @@ -9,9 +11,9 @@ - + - + @@ -19,6 +21,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WeScanSampleProject/EditImageViewController.swift b/WeScanSampleProject/EditImageViewController.swift new file mode 100644 index 00000000..7fca1e55 --- /dev/null +++ b/WeScanSampleProject/EditImageViewController.swift @@ -0,0 +1,46 @@ +// +// EditImageViewController.swift +// WeScanSampleProject +// +// Created by Chawatvish Worrapoj on 8/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit +import WeScan + +final class EditImageViewController: UIViewController { + + @IBOutlet private weak var editImageView: UIView! + var captureImage: UIImage! + var quad: Quadrilateral? + var controller: WeScan.EditImageViewController! + + 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() + } +} + +extension EditImageViewController: EditImageViewDelegate { + func cropped(image: UIImage) { + guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "ReviewImageView") as? ReviewImageViewController else { return } + controller.modalPresentationStyle = .fullScreen + controller.image = image + navigationController?.pushViewController(controller, animated: false) + } +} diff --git a/WeScanSampleProject/HomeViewController.swift b/WeScanSampleProject/HomeViewController.swift index e58fb144..9dd02f1a 100644 --- a/WeScanSampleProject/HomeViewController.swift +++ b/WeScanSampleProject/HomeViewController.swift @@ -97,6 +97,12 @@ final class HomeViewController: UIViewController { @objc func scanOrSelectImage(_ sender: UIButton) { let actionSheet = UIAlertController(title: "Would you like to scan an image or select one from your photo library?", message: nil, preferredStyle: .actionSheet) + let newAction = UIAlertAction(title: "A new scan", style: .default) { (_) in + guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "NewCameraViewController") else { return } + controller.modalPresentationStyle = .fullScreen + self.present(controller, animated: true, completion: nil) + } + let scanAction = UIAlertAction(title: "Scan", style: .default) { (_) in self.scanImage() } @@ -110,6 +116,7 @@ final class HomeViewController: UIViewController { actionSheet.addAction(scanAction) actionSheet.addAction(selectAction) actionSheet.addAction(cancelAction) + actionSheet.addAction(newAction) present(actionSheet, animated: true) } diff --git a/WeScanSampleProject/Info.plist b/WeScanSampleProject/Info.plist index 7d6974ce..532f89ea 100644 --- a/WeScanSampleProject/Info.plist +++ b/WeScanSampleProject/Info.plist @@ -22,6 +22,8 @@ NSCameraUsageDescription Used to scan images. + NSPhotoLibraryUsageDescription + Used to select images from your Photo Library. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -37,8 +39,6 @@ UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown - NSPhotoLibraryUsageDescription - Used to select images from your Photo Library. UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/WeScanSampleProject/NewCameraViewController.swift b/WeScanSampleProject/NewCameraViewController.swift new file mode 100644 index 00000000..dfda8c5e --- /dev/null +++ b/WeScanSampleProject/NewCameraViewController.swift @@ -0,0 +1,54 @@ +// +// NewCameraViewController.swift +// WeScanSampleProject +// +// Created by Chawatvish Worrapoj on 7/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit +import WeScan + +final class NewCameraViewController: UIViewController { + + @IBOutlet private weak var cameraView: UIView! + var controller: CameraScannerViewController! + + override func viewDidLoad() { + super.viewDidLoad() + setupView() + } + + private func setupView() { + controller = CameraScannerViewController() + controller.view.frame = cameraView.bounds + controller.willMove(toParent: self) + cameraView.addSubview(controller.view) + self.addChild(controller) + controller.didMove(toParent: self) + controller.delegate = self + } + + @IBAction func flashTapped(_ sender: UIButton) { + controller.toggleFlash() + } + + @IBAction func captureTapped(_ sender: UIButton) { + controller.capture() + } + +} + +extension NewCameraViewController: CameraScannerViewOutputDelegate { + func captureImageFailWithError(error: Error) { + print(error) + } + + func captureImageSuccess(image: UIImage, withQuad quad: Quadrilateral?) { + guard let controller = self.storyboard?.instantiateViewController(withIdentifier: "NewEditImageView") as? EditImageViewController else { return } + controller.modalPresentationStyle = .fullScreen + controller.captureImage = image + controller.quad = quad + navigationController?.pushViewController(controller, animated: false) + } +} diff --git a/WeScanSampleProject/ReviewImageViewController.swift b/WeScanSampleProject/ReviewImageViewController.swift new file mode 100644 index 00000000..a76a1bfa --- /dev/null +++ b/WeScanSampleProject/ReviewImageViewController.swift @@ -0,0 +1,21 @@ +// +// ReviewImageViewController.swift +// WeScanSampleProject +// +// Created by Chawatvish Worrapoj on 8/1/2020 +// Copyright © 2020 WeTransfer. All rights reserved. +// + +import UIKit + +final class ReviewImageViewController: UIViewController { + + @IBOutlet private weak var imageView: UIImageView! + var image: UIImage? + + override func viewDidLoad() { + super.viewDidLoad() + guard let image = image else { return } + imageView.image = image + } +}