Quadrilateral.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. //
  2. // Quadrilateral.swift
  3. // WeScan
  4. //
  5. // Created by Boris Emorine on 2/8/18.
  6. // Copyright © 2018 WeTransfer. All rights reserved.
  7. //
  8. import Foundation
  9. import AVFoundation
  10. /// A data structure representing a quadrilateral and its position. This class exists to bypass the fact that CIRectangleFeature is read-only.
  11. public struct Quadrilateral: Transformable {
  12. /// A point that specifies the top left corner of the quadrilateral.
  13. var topLeft: CGPoint
  14. /// A point that specifies the top right corner of the quadrilateral.
  15. var topRight: CGPoint
  16. /// A point that specifies the bottom right corner of the quadrilateral.
  17. var bottomRight: CGPoint
  18. /// A point that specifies the bottom left corner of the quadrilateral.
  19. var bottomLeft: CGPoint
  20. init(rectangleFeature: CIRectangleFeature) {
  21. self.topLeft = rectangleFeature.topLeft
  22. self.topRight = rectangleFeature.topRight
  23. self.bottomLeft = rectangleFeature.bottomLeft
  24. self.bottomRight = rectangleFeature.bottomRight
  25. }
  26. init(topLeft: CGPoint, topRight: CGPoint, bottomRight: CGPoint, bottomLeft: CGPoint) {
  27. self.topLeft = topLeft
  28. self.topRight = topRight
  29. self.bottomRight = bottomRight
  30. self.bottomLeft = bottomLeft
  31. }
  32. /// Generates a `UIBezierPath` of the quadrilateral.
  33. func path() -> UIBezierPath {
  34. let path = UIBezierPath()
  35. path.move(to: topLeft)
  36. path.addLine(to: topRight)
  37. path.addLine(to: bottomRight)
  38. path.addLine(to: bottomLeft)
  39. path.close()
  40. return path
  41. }
  42. /// Applies a `CGAffineTransform` to the quadrilateral.
  43. ///
  44. /// - Parameters:
  45. /// - t: the transform to apply.
  46. /// - Returns: The transformed quadrilateral.
  47. func applying(_ transform: CGAffineTransform) -> Quadrilateral {
  48. let quadrilateral = Quadrilateral(topLeft: topLeft.applying(transform), topRight: topRight.applying(transform), bottomRight: bottomRight.applying(transform), bottomLeft: bottomLeft.applying(transform))
  49. return quadrilateral
  50. }
  51. /// Reorganizes the current quadrilateal, 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.
  52. mutating func reorganize() {
  53. let points = [topLeft, topRight, bottomRight, bottomLeft]
  54. let ySortedPoints = sortPointsByYValue(points)
  55. guard ySortedPoints.count == 4 else {
  56. return
  57. }
  58. let topMostPoints = Array(ySortedPoints[0..<2])
  59. let bottomMostPoints = Array(ySortedPoints[2..<4])
  60. let xSortedTopMostPoints = sortPointsByXValue(topMostPoints)
  61. let xSortedBottomMostPoints = sortPointsByXValue(bottomMostPoints)
  62. guard xSortedTopMostPoints.count > 1,
  63. xSortedBottomMostPoints.count > 1 else {
  64. return
  65. }
  66. topLeft = xSortedTopMostPoints[0]
  67. topRight = xSortedTopMostPoints[1]
  68. bottomRight = xSortedBottomMostPoints[1]
  69. bottomLeft = xSortedBottomMostPoints[0]
  70. }
  71. /// Scales the quadrilateral based on the ratio of two given sizes, and optionnaly applies a rotation.
  72. ///
  73. /// - Parameters:
  74. /// - fromSize: The size the quadrilateral is currently related to.
  75. /// - toSize: The size to scale the quadrilateral to.
  76. /// - rotationAngle: The optional rotation to apply.
  77. /// - Returns: The newly scaled and potentially rotated quadrilateral.
  78. func scale(_ fromSize: CGSize, _ toSize: CGSize, withRotationAngle rotationAngle: CGFloat = 0.0) -> Quadrilateral {
  79. var invertedfromSize = fromSize
  80. let rotated = rotationAngle != 0.0
  81. if rotated && rotationAngle != CGFloat.pi {
  82. invertedfromSize = CGSize(width: fromSize.height, height: fromSize.width)
  83. }
  84. var transformedQuad = self
  85. let invertedFromSizeWidth = invertedfromSize.width == 0 ? .leastNormalMagnitude : invertedfromSize.width
  86. let scale = toSize.width / invertedFromSizeWidth
  87. let scaledTransform = CGAffineTransform(scaleX: scale, y: scale)
  88. transformedQuad = transformedQuad.applying(scaledTransform)
  89. if rotated {
  90. let rotationTransform = CGAffineTransform(rotationAngle: rotationAngle)
  91. let fromImageBounds = CGRect(x: 0.0, y: 0.0, width: fromSize.width, height: fromSize.height).applying(scaledTransform).applying(rotationTransform)
  92. let toImageBounds = CGRect(x: 0.0, y: 0.0, width: toSize.width, height: toSize.height)
  93. let translationTransform = CGAffineTransform.translateTransform(fromCenterOfRect: fromImageBounds, toCenterOfRect: toImageBounds)
  94. transformedQuad = transformedQuad.applyTransforms([rotationTransform, translationTransform])
  95. }
  96. return transformedQuad
  97. }
  98. // Convenience functions
  99. /// Sorts the given `CGPoints` based on their y value.
  100. /// - Parameters:
  101. /// - points: The poinmts to sort.
  102. /// - Returns: The points sorted based on their y value.
  103. private func sortPointsByYValue(_ points: [CGPoint]) -> [CGPoint] {
  104. return points.sorted { (point1, point2) -> Bool in
  105. point1.y < point2.y
  106. }
  107. }
  108. /// Sorts the given `CGPoints` based on their x value.
  109. /// - Parameters:
  110. /// - points: The poinmts to sort.
  111. /// - Returns: The points sorted based on their x value.
  112. private func sortPointsByXValue(_ points: [CGPoint]) -> [CGPoint] {
  113. return points.sorted { (point1, point2) -> Bool in
  114. point1.x < point2.x
  115. }
  116. }
  117. }
  118. extension Quadrilateral {
  119. /// Converts the current to the cartesian coordinate system (where 0 on the y axis is at the bottom).
  120. ///
  121. /// - Parameters:
  122. /// - height: The height of the rect containing the quadrilateral.
  123. /// - Returns: The same quadrilateral in the cartesian corrdinate system.
  124. func toCartesian(withHeight height: CGFloat) -> Quadrilateral {
  125. let topLeft = self.topLeft.cartesian(withHeight: height)
  126. let topRight = self.topRight.cartesian(withHeight: height)
  127. let bottomRight = self.bottomRight.cartesian(withHeight: height)
  128. let bottomLeft = self.bottomLeft.cartesian(withHeight: height)
  129. return Quadrilateral(topLeft: topLeft, topRight: topRight, bottomRight: bottomRight, bottomLeft: bottomLeft)
  130. }
  131. }
  132. extension Quadrilateral: Equatable {
  133. public static func == (lhs: Quadrilateral, rhs: Quadrilateral) -> Bool {
  134. return lhs.topLeft == rhs.topLeft && lhs.topRight == rhs.topRight && lhs.bottomRight == rhs.bottomRight && lhs.bottomLeft == rhs.bottomLeft
  135. }
  136. }