diff options
author | Ben <31181527+benedom@users.noreply.github.com> | 2024-02-27 09:08:15 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-27 09:08:15 +0100 |
commit | b3dbe07734759677e3ce2d6d090249738fdbc882 (patch) | |
tree | 44101318b0622cce4c8791b282962118d437e073 /Sources | |
parent | 29a39f48c6d81eb66df008699259cc7fead3b0a1 (diff) | |
parent | b1f3174b0a363e30ddda0d15f819dc9b0bef5fc9 (diff) |
Merge pull request #4 from leoz/master
Rotate image
Diffstat (limited to 'Sources')
-rw-r--r-- | Sources/SwiftyCrop/Models/CropViewModel.swift | 64 | ||||
-rw-r--r-- | Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift | 7 | ||||
-rw-r--r-- | Sources/SwiftyCrop/View/CropView.swift | 100 |
3 files changed, 129 insertions, 42 deletions
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, @@ -122,6 +123,47 @@ class CropViewModel: ObservableObject { } /** + 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: - image: The UIImage to calculate the rectangle to crop for @@ -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) } } |