123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- //
- // EditScanViewController.swift
- // WeScan
- //
- // Created by Boris Emorine on 2/12/18.
- // Copyright © 2018 WeTransfer. All rights reserved.
- //
- import UIKit
- import AVFoundation
- @available(iOS 10, *)
- /// The `EditScanViewController` offers an interface for the user to edit the detected quadrilateral.
- final class EditScanViewController: UIViewController {
-
- lazy private 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
- }()
-
- lazy private var quadView: QuadrilateralView = {
- let quadView = QuadrilateralView()
- quadView.editable = true
- quadView.translatesAutoresizingMaskIntoConstraints = false
- return quadView
- }()
-
- lazy private var nextButton: UIBarButtonItem = {
- let title = NSLocalizedString("wescan.edit.button.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
- }()
- /// 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?) {
- self.image = image.applyingPortraitOrientation()
- 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 func viewDidLoad() {
- super.viewDidLoad()
-
- setupViews()
- setupConstraints()
- title = NSLocalizedString("wescan.edit.title", comment: "The title of the EditScanViewController")
- navigationItem.rightBarButtonItem = nextButton
-
- zoomGestureController = ZoomGestureController(image: image, quadView: quadView)
-
- let touchDown = UILongPressGestureRecognizer(target:zoomGestureController, action: #selector(zoomGestureController.handle(pan:)))
- touchDown.minimumPressDuration = 0
- view.addGestureRecognizer(touchDown)
- }
-
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- adjustQuadViewConstraints()
- displayQuad()
- }
-
- override 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 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 scaledQuad = quad.scale(quadView.bounds.size, image.size)
- self.quad = scaledQuad
-
- var cartesianScaledQuad = scaledQuad.toCartesian(withHeight: image.size.height)
- cartesianScaledQuad.reorganize()
-
- let filteredImage = ciImage.applyingFilter("CIPerspectiveCorrection", parameters: [
- "inputTopLeft": CIVector(cgPoint: cartesianScaledQuad.bottomLeft),
- "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight),
- "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft),
- "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight)
- ])
-
- var uiImage: UIImage!
-
- // Let's try to generate the CGImage from the CIImage before creating a UIImage.
- if let cgImage = CIContext(options: nil).createCGImage(filteredImage, from: filteredImage.extent) {
- uiImage = UIImage(cgImage: cgImage)
- } else {
- uiImage = UIImage(ciImage: filteredImage, scale: 1.0, orientation: .up)
- }
-
- let results = ImageScannerResults(originalImage: image, scannedImage: uiImage, detectedRectangle: scaledQuad)
- let reviewViewController = ReviewViewController(results: results)
-
- navigationController?.pushViewController(reviewViewController, animated: true)
- }
- private func displayQuad() {
- let imageSize = image.size
- let imageFrame = CGRect(x: quadView.frame.origin.x, y: quadView.frame.origin.y, 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 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
- }
- }
|