diff --git a/Demo/SwiftyCropDemo/ContentView.swift b/Demo/SwiftyCropDemo/ContentView.swift index c999dd3..cfb1f01 100644 --- a/Demo/SwiftyCropDemo/ContentView.swift +++ b/Demo/SwiftyCropDemo/ContentView.swift @@ -13,6 +13,7 @@ struct ContentView: View { @State private var selectedImage: UIImage? @State private var selectedShape: MaskShape = .square @State private var cropImageCircular: Bool = false + @State private var rotateImage: Bool = true var body: some View { VStack { @@ -56,6 +57,7 @@ struct ContentView: View { ) } Toggle("Crop image to circle", isOn: $cropImageCircular) + Toggle("Rotate image", isOn: $rotateImage) } .buttonStyle(.bordered) .padding() @@ -69,7 +71,8 @@ struct ContentView: View { imageToCrop: selectedImage, maskShape: selectedShape, configuration: SwiftyCropConfiguration( - cropImageCircular: cropImageCircular + cropImageCircular: cropImageCircular, + rotateImage: rotateImage ) ) { croppedImage in // Do something with the returned, cropped image diff --git a/Sources/SwiftyCrop/Models/CropViewModel.swift b/Sources/SwiftyCrop/Models/CropViewModel.swift index d470d29..9d5e16d 100644 --- a/Sources/SwiftyCrop/Models/CropViewModel.swift +++ b/Sources/SwiftyCrop/Models/CropViewModel.swift @@ -10,7 +10,8 @@ class CropViewModel: ObservableObject { @Published var lastScale: CGFloat = 1.0 @Published var offset: CGSize = .zero @Published var lastOffset: CGSize = .zero - @Published var circleSize: CGSize = .zero + @Published var angle: Angle = Angle(degrees: 0) + @Published var lastAngle: Angle = Angle(degrees: 0) init( maskRadius: CGFloat, @@ -121,6 +122,47 @@ class CropViewModel: ObservableObject { return circleCroppedImage } + /** + Rotates the image to the angle that is rotated inside the view. + - Parameters: + - image: The UIImage to rotate + - angle: The Angle to rotate to + - Returns: A rotated UIImage if the rotating operation is successful; otherwise nil. + */ + func rotate(_ image: UIImage, _ angle: Angle) -> UIImage? { + guard let orientedImage = image.correctlyOriented else { + return nil + } + + guard let cgImage = orientedImage.cgImage else { + return nil + } + + let ciImage = CIImage(cgImage: cgImage) + + // Prepare filter + let filter = CIFilter.straightenFilter( + image: ciImage, + radians: angle.radians + ) + + // Get output image + guard let output = filter?.outputImage else { + return nil + } + + // Create resulting image + let context = CIContext() + guard let result = context.createCGImage( + output, + from: output.extent + ) else { + return nil + } + + return UIImage(cgImage: result) + } + /** Calculates the rectangle to crop. - Parameters: @@ -177,3 +219,23 @@ private extension UIImage { return normalizedImage } } + +private extension CIFilter { + /** + Creates the straighten filter. + - Parameters: + - inputImage: The CIImage to use as an input image + - radians: An angle in radians + - Returns: A generated CIFilter. + */ + static func straightenFilter(image: CIImage, radians: Double) -> CIFilter? { + let angle: Double = radians != 0 ? -radians : 0 + guard let filter = CIFilter(name: "CIStraightenFilter") else { + return nil + } + filter.setDefaults() + filter.setValue(image, forKey: kCIInputImageKey) + filter.setValue(angle, forKey: kCIInputAngleKey) + return filter + } +} diff --git a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift index 73566be..b41ea53 100644 --- a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift +++ b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift @@ -5,6 +5,7 @@ public struct SwiftyCropConfiguration { let maxMagnificationScale: CGFloat let maskRadius: CGFloat let cropImageCircular: Bool + let rotateImage: Bool /// Creates a new instance of `SwiftyCropConfiguration`. /// @@ -15,13 +16,17 @@ public struct SwiftyCropConfiguration { /// Defaults to `130`. /// - cropImageCircular: Option to enable circular crop. /// Defaults to `false`. + /// - rotateImage: Option to rotate image. + /// Defaults to `true`. public init( maxMagnificationScale: CGFloat = 4.0, maskRadius: CGFloat = 130, - cropImageCircular: Bool = false + cropImageCircular: Bool = false, + rotateImage: Bool = true ) { self.maxMagnificationScale = maxMagnificationScale self.maskRadius = maskRadius self.cropImageCircular = cropImageCircular + self.rotateImage = rotateImage } } diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index 389047e..a61aa47 100644 --- a/Sources/SwiftyCrop/View/CropView.swift +++ b/Sources/SwiftyCrop/View/CropView.swift @@ -30,6 +30,49 @@ struct CropView: View { } var body: some View { + let magnificationGesture = MagnificationGesture() + .onChanged { value in + let sensitivity: CGFloat = 0.2 + let scaledValue = (value.magnitude - 1) * sensitivity + 1 + + let maxScaleValues = viewModel.calculateMagnificationGestureMaxValues() + viewModel.scale = min(max(scaledValue * viewModel.scale, maxScaleValues.0), maxScaleValues.1) + + let maxOffsetPoint = viewModel.calculateDragGestureMax() + let newX = min(max(viewModel.lastOffset.width, -maxOffsetPoint.x), maxOffsetPoint.x) + let newY = min(max(viewModel.lastOffset.height, -maxOffsetPoint.y), maxOffsetPoint.y) + viewModel.offset = CGSize(width: newX, height: newY) + } + .onEnded { _ in + viewModel.lastScale = viewModel.scale + viewModel.lastOffset = viewModel.offset + } + + let dragGesture = DragGesture() + .onChanged { value in + let maxOffsetPoint = viewModel.calculateDragGestureMax() + let newX = min( + max(value.translation.width + viewModel.lastOffset.width, -maxOffsetPoint.x), + maxOffsetPoint.x + ) + let newY = min( + max(value.translation.height + viewModel.lastOffset.height, -maxOffsetPoint.y), + maxOffsetPoint.y + ) + viewModel.offset = CGSize(width: newX, height: newY) + } + .onEnded { _ in + viewModel.lastOffset = viewModel.offset + } + + let rotationGesture = RotationGesture() + .onChanged { value in + viewModel.angle = value + } + .onEnded { _ in + viewModel.lastAngle = viewModel.angle + } + VStack { Text("interaction_instructions", tableName: localizableTableName, bundle: .module) .font(.system(size: 16, weight: .regular)) @@ -41,6 +84,7 @@ struct CropView: View { Image(uiImage: image) .resizable() .scaledToFit() + .rotationEffect(viewModel.angle) .scaleEffect(viewModel.scale) .offset(viewModel.offset) .opacity(0.5) @@ -56,6 +100,7 @@ struct CropView: View { Image(uiImage: image) .resizable() .scaledToFit() + .rotationEffect(viewModel.angle) .scaleEffect(viewModel.scale) .offset(viewModel.offset) .mask( @@ -64,43 +109,9 @@ struct CropView: View { ) } .frame(maxWidth: .infinity, maxHeight: .infinity) - .gesture( - MagnificationGesture() - .onChanged { value in - let sensitivity: CGFloat = 0.2 - let scaledValue = (value.magnitude - 1) * sensitivity + 1 - - let maxScaleValues = viewModel.calculateMagnificationGestureMaxValues() - viewModel.scale = min(max(scaledValue * viewModel.scale, maxScaleValues.0), maxScaleValues.1) - - let maxOffsetPoint = viewModel.calculateDragGestureMax() - let newX = min(max(viewModel.lastOffset.width, -maxOffsetPoint.x), maxOffsetPoint.x) - let newY = min(max(viewModel.lastOffset.height, -maxOffsetPoint.y), maxOffsetPoint.y) - viewModel.offset = CGSize(width: newX, height: newY) - } - .onEnded { _ in - viewModel.lastScale = viewModel.scale - viewModel.lastOffset = viewModel.offset - } - .simultaneously( - with: DragGesture() - .onChanged { value in - let maxOffsetPoint = viewModel.calculateDragGestureMax() - let newX = min( - max(value.translation.width + viewModel.lastOffset.width, -maxOffsetPoint.x), - maxOffsetPoint.x - ) - let newY = min( - max(value.translation.height + viewModel.lastOffset.height, -maxOffsetPoint.y), - maxOffsetPoint.y - ) - viewModel.offset = CGSize(width: newX, height: newY) - } - .onEnded { _ in - viewModel.lastOffset = viewModel.offset - } - ) - ) + .simultaneousGesture(magnificationGesture) + .simultaneousGesture(dragGesture) + .simultaneousGesture(configuration.rotateImage ? rotationGesture : nil) HStack { Button { @@ -127,10 +138,19 @@ struct CropView: View { } private func cropImage() -> UIImage? { - if maskShape == .circle && configuration.cropImageCircular { - viewModel.cropToCircle(image) + var editedImage: UIImage = image + if configuration.rotateImage { + if let rotatedImage: UIImage = viewModel.rotate( + editedImage, + viewModel.lastAngle + ) { + editedImage = rotatedImage + } + } + if configuration.cropImageCircular && maskShape == .circle { + return viewModel.cropToCircle(editedImage) } else { - viewModel.cropToSquare(image) + return viewModel.cropToSquare(editedImage) } }