From 0156c27aabfd82e6450604a8f32c74a58bcff8f9 Mon Sep 17 00:00:00 2001 From: leoz Date: Mon, 22 Jan 2024 21:14:12 -0500 Subject: [PATCH 1/2] Crop image to circle --- .gitignore | 3 + Sources/SwiftyCrop/Models/CropViewModel.swift | 87 +++++++++++++++++-- Sources/SwiftyCrop/View/CropView.swift | 10 ++- 3 files changed, 91 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 860ff62..9a042de 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ xcuserdata/ /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings +### Visual Studio Code ### +.build + # End of https://www.toptal.com/developers/gitignore/api/macos,xcode \ No newline at end of file diff --git a/Sources/SwiftyCrop/Models/CropViewModel.swift b/Sources/SwiftyCrop/Models/CropViewModel.swift index e27f1ad..761478a 100644 --- a/Sources/SwiftyCrop/Models/CropViewModel.swift +++ b/Sources/SwiftyCrop/Models/CropViewModel.swift @@ -40,15 +40,91 @@ class CropViewModel: ObservableObject { } /** - Crops the image to the part that is dragged/zoomed inside the view. Cropped image will **always** be a square, no matter what mask shape is used. + Crops the image to the part that is dragged/zoomed inside the view. Cropped image will be a square. - Parameters: - image: The UIImage to crop - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. */ - func crop(_ image: UIImage) -> UIImage? { + func cropToSquare(_ image: UIImage) -> UIImage? { guard let orientedImage = image.correctlyOriented else { return nil } + + let cropRect = calculateCropRect(orientedImage) + + guard let cgImage = orientedImage.cgImage, + let result = cgImage.cropping(to: cropRect) else { + return nil + } + + return UIImage(cgImage: result) + } + + /** + Crops the image to the part that is dragged/zoomed inside the view. Cropped image will be a circle. + - Parameters: + - image: The UIImage to crop + - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. + */ + func cropToCircle(_ image: UIImage) -> UIImage? { + guard let orientedImage = image.correctlyOriented else { + return nil + } + + let cropRect = calculateCropRect(orientedImage) + + // A circular crop results in some transparency in the + // cropped image, so set opaque to false to ensure the + // cropped image does not include a background fill + let imageRendererFormat = orientedImage.imageRendererFormat + imageRendererFormat.opaque = false + + // UIGraphicsImageRenderer().image provides a block + // interface to draw into in a new UIImage + let circleCroppedImage = UIGraphicsImageRenderer( + // The cropRect.size is the size of + // the resulting circleCroppedImage + size: cropRect.size, + format: imageRendererFormat).image { context in + + // The drawRect is the cropRect starting at (0,0) + let drawRect = CGRect( + origin: .zero, + size: cropRect.size + ) + + // addClip on a UIBezierPath will clip all contents + // outside of the UIBezierPath drawn after addClip + // is called, in this case, drawRect is a circle so + // the UIBezierPath clips drawing to the circle + UIBezierPath(ovalIn: drawRect).addClip() + + // The drawImageRect is offsets the image’s bounds + // such that the circular clip is at the center of + // the image + let drawImageRect = CGRect( + origin: CGPoint( + x: -cropRect.origin.x, + y: -cropRect.origin.y + ), + size: orientedImage.size + ) + + // Draws the orientedImage inside of the + // circular clip + orientedImage.draw(in: drawImageRect) + } + + return circleCroppedImage + } + + /** + Calculates the rectangle to crop. + - Parameters: + - image: The UIImage to calculate the rectangle to crop for + - Returns: A CGRect representing the rectangle to crop. + */ + private func calculateCropRect(_ orientedImage: UIImage) -> CGRect { // The relation factor of the originals image width/height and the width/height of the image displayed in the view (initial) let factor = min((orientedImage.size.width / imageSizeInView.width), (orientedImage.size.height / imageSizeInView.height)) let centerInOriginalImage = CGPoint(x: orientedImage.size.width / 2, y: orientedImage.size.height / 2) @@ -73,12 +149,7 @@ class CropViewModel: ObservableObject { height: cropRectDimension ) - guard let cgImage = orientedImage.cgImage, - let result = cgImage.cropping(to: cropRect) else { - return nil - } - - return UIImage(cgImage: result) + return cropRect } } diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index 3cb453a..05b1a66 100644 --- a/Sources/SwiftyCrop/View/CropView.swift +++ b/Sources/SwiftyCrop/View/CropView.swift @@ -107,7 +107,7 @@ struct CropView: View { Spacer() Button { - onComplete(viewModel.crop(image)) + onComplete(cropImage()) dismiss() } label: { Text("save_button", tableName: localizableTableName, bundle: .module) @@ -120,6 +120,14 @@ struct CropView: View { .background(.black) } + private func cropImage() -> UIImage? { + if maskShape == .circle { + viewModel.cropToCircle(image) + } else { + viewModel.cropToSquare(image) + } + } + private struct MaskShapeView: View { let maskShape: MaskShape From c59a4cf49bfd0e663bbe77bd4e4ae9bf073356a7 Mon Sep 17 00:00:00 2001 From: leoz Date: Tue, 23 Jan 2024 10:24:16 -0500 Subject: [PATCH 2/2] Make circular crop optional --- .../Models/SwiftyCropConfiguration.swift | 18 ++++++++++++++---- Sources/SwiftyCrop/View/CropView.swift | 2 +- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift index cc3d47a..73566be 100644 --- a/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift +++ b/Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift @@ -4,14 +4,24 @@ import CoreGraphics public struct SwiftyCropConfiguration { let maxMagnificationScale: CGFloat let maskRadius: CGFloat - + let cropImageCircular: Bool + /// Creates a new instance of `SwiftyCropConfiguration`. /// /// - Parameters: - /// - maxMagnificationScale: The maximum scale factor that the image can be magnified while cropping. Defaults to `4.0`. - /// - maskRadius: The radius of the mask used for cropping. Defaults to `130`. - public init(maxMagnificationScale: CGFloat = 4.0, maskRadius: CGFloat = 130) { + /// - maxMagnificationScale: The maximum scale factor that the image can be magnified while cropping. + /// Defaults to `4.0`. + /// - maskRadius: The radius of the mask used for cropping. + /// Defaults to `130`. + /// - cropImageCircular: Option to enable circular crop. + /// Defaults to `false`. + public init( + maxMagnificationScale: CGFloat = 4.0, + maskRadius: CGFloat = 130, + cropImageCircular: Bool = false + ) { self.maxMagnificationScale = maxMagnificationScale self.maskRadius = maskRadius + self.cropImageCircular = cropImageCircular } } diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index 05b1a66..d593c5d 100644 --- a/Sources/SwiftyCrop/View/CropView.swift +++ b/Sources/SwiftyCrop/View/CropView.swift @@ -121,7 +121,7 @@ struct CropView: View { } private func cropImage() -> UIImage? { - if maskShape == .circle { + if maskShape == .circle && configuration.cropImageCircular { viewModel.cropToCircle(image) } else { viewModel.cropToSquare(image)