EditScanViewController.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. //
  2. // EditScanViewController.swift
  3. // WeScan
  4. //
  5. // Created by Boris Emorine on 2/12/18.
  6. // Copyright © 2018 WeTransfer. All rights reserved.
  7. //
  8. import UIKit
  9. import AVFoundation
  10. @available(iOS 10, *)
  11. /// The `EditScanViewController` offers an interface for the user to edit the detected quadrilateral.
  12. final class EditScanViewController: UIViewController {
  13. lazy private var imageView: UIImageView = {
  14. let imageView = UIImageView()
  15. imageView.clipsToBounds = true
  16. imageView.isOpaque = true
  17. imageView.image = image
  18. imageView.backgroundColor = .black
  19. imageView.contentMode = .scaleAspectFit
  20. imageView.translatesAutoresizingMaskIntoConstraints = false
  21. return imageView
  22. }()
  23. lazy private var quadView: QuadrilateralView = {
  24. let quadView = QuadrilateralView()
  25. quadView.editable = true
  26. quadView.translatesAutoresizingMaskIntoConstraints = false
  27. return quadView
  28. }()
  29. lazy private var nextButton: UIBarButtonItem = {
  30. let title = NSLocalizedString("wescan.edit.button.next", comment: "A generic next button")
  31. let button = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(pushReviewController))
  32. button.tintColor = navigationController?.navigationBar.tintColor
  33. return button
  34. }()
  35. /// The image the quadrilateral was detected on.
  36. private let image: UIImage
  37. /// The detected quadrilateral that can be edited by the user. Uses the image's coordinates.
  38. private var quad: Quadrilateral
  39. private var zoomGestureController: ZoomGestureController!
  40. private var quadViewWidthConstraint = NSLayoutConstraint()
  41. private var quadViewHeightConstraint = NSLayoutConstraint()
  42. // MARK: - Life Cycle
  43. init(image: UIImage, quad: Quadrilateral?) {
  44. self.image = image.applyingPortraitOrientation()
  45. self.quad = quad ?? EditScanViewController.defaultQuad(forImage: image)
  46. super.init(nibName: nil, bundle: nil)
  47. }
  48. required init?(coder aDecoder: NSCoder) {
  49. fatalError("init(coder:) has not been implemented")
  50. }
  51. override func viewDidLoad() {
  52. super.viewDidLoad()
  53. setupViews()
  54. setupConstraints()
  55. title = NSLocalizedString("wescan.edit.title", comment: "The title of the EditScanViewController")
  56. navigationItem.rightBarButtonItem = nextButton
  57. zoomGestureController = ZoomGestureController(image: image, quadView: quadView)
  58. let touchDown = UILongPressGestureRecognizer(target:zoomGestureController, action: #selector(zoomGestureController.handle(pan:)))
  59. touchDown.minimumPressDuration = 0
  60. view.addGestureRecognizer(touchDown)
  61. }
  62. override func viewDidLayoutSubviews() {
  63. super.viewDidLayoutSubviews()
  64. adjustQuadViewConstraints()
  65. displayQuad()
  66. }
  67. override func viewWillDisappear(_ animated: Bool) {
  68. super.viewWillDisappear(animated)
  69. // Work around for an iOS 11.2 bug where UIBarButtonItems don't get back to their normal state after being pressed.
  70. navigationController?.navigationBar.tintAdjustmentMode = .normal
  71. navigationController?.navigationBar.tintAdjustmentMode = .automatic
  72. }
  73. // MARK: - Setups
  74. private func setupViews() {
  75. view.addSubview(imageView)
  76. view.addSubview(quadView)
  77. }
  78. private func setupConstraints() {
  79. let imageViewConstraints = [
  80. imageView.topAnchor.constraint(equalTo: view.topAnchor),
  81. imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  82. view.bottomAnchor.constraint(equalTo: imageView.bottomAnchor),
  83. view.leadingAnchor.constraint(equalTo: imageView.leadingAnchor)
  84. ]
  85. quadViewWidthConstraint = quadView.widthAnchor.constraint(equalToConstant: 0.0)
  86. quadViewHeightConstraint = quadView.heightAnchor.constraint(equalToConstant: 0.0)
  87. let quadViewConstraints = [
  88. quadView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
  89. quadView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
  90. quadViewWidthConstraint,
  91. quadViewHeightConstraint
  92. ]
  93. NSLayoutConstraint.activate(quadViewConstraints + imageViewConstraints)
  94. }
  95. // MARK: - Actions
  96. @objc func pushReviewController() {
  97. guard let quad = quadView.quad,
  98. let ciImage = CIImage(image: image) else {
  99. if let imageScannerController = navigationController as? ImageScannerController {
  100. let error = ImageScannerControllerError.ciImageCreation
  101. imageScannerController.imageScannerDelegate?.imageScannerController(imageScannerController, didFailWithError: error)
  102. }
  103. return
  104. }
  105. let scaledQuad = quad.scale(quadView.bounds.size, image.size)
  106. self.quad = scaledQuad
  107. var cartesianScaledQuad = scaledQuad.toCartesian(withHeight: image.size.height)
  108. cartesianScaledQuad.reorganize()
  109. let filteredImage = ciImage.applyingFilter("CIPerspectiveCorrection", parameters: [
  110. "inputTopLeft": CIVector(cgPoint: cartesianScaledQuad.bottomLeft),
  111. "inputTopRight": CIVector(cgPoint: cartesianScaledQuad.bottomRight),
  112. "inputBottomLeft": CIVector(cgPoint: cartesianScaledQuad.topLeft),
  113. "inputBottomRight": CIVector(cgPoint: cartesianScaledQuad.topRight)
  114. ])
  115. var uiImage: UIImage!
  116. // Let's try to generate the CGImage from the CIImage before creating a UIImage.
  117. if let cgImage = CIContext(options: nil).createCGImage(filteredImage, from: filteredImage.extent) {
  118. uiImage = UIImage(cgImage: cgImage)
  119. } else {
  120. uiImage = UIImage(ciImage: filteredImage, scale: 1.0, orientation: .up)
  121. }
  122. let results = ImageScannerResults(originalImage: image, scannedImage: uiImage, detectedRectangle: scaledQuad)
  123. let reviewViewController = ReviewViewController(results: results)
  124. navigationController?.pushViewController(reviewViewController, animated: true)
  125. }
  126. private func displayQuad() {
  127. let imageSize = image.size
  128. let imageFrame = CGRect(x: quadView.frame.origin.x, y: quadView.frame.origin.y, width: quadViewWidthConstraint.constant, height: quadViewHeightConstraint.constant)
  129. let scaleTransform = CGAffineTransform.scaleTransform(forSize: imageSize, aspectFillInSize: imageFrame.size)
  130. let transforms = [scaleTransform]
  131. let transformedQuad = quad.applyTransforms(transforms)
  132. quadView.drawQuadrilateral(quad: transformedQuad, animated: false)
  133. }
  134. /// The quadView should be lined up on top of the actual image displayed by the imageView.
  135. /// 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.
  136. private func adjustQuadViewConstraints() {
  137. let frame = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
  138. quadViewWidthConstraint.constant = frame.size.width
  139. quadViewHeightConstraint.constant = frame.size.height
  140. }
  141. /// Generates a `Quadrilateral` object that's centered and one third of the size of the passed in image.
  142. private static func defaultQuad(forImage image: UIImage) -> Quadrilateral {
  143. let topLeft = CGPoint(x: image.size.width / 3.0, y: image.size.height / 3.0)
  144. let topRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: image.size.height / 3.0)
  145. let bottomRight = CGPoint(x: 2.0 * image.size.width / 3.0, y: 2.0 * image.size.height / 3.0)
  146. let bottomLeft = CGPoint(x: image.size.width / 3.0, y: 2.0 * image.size.height / 3.0)
  147. let quad = Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
  148. return quad
  149. }
  150. }