Skip to content

Commit

Permalink
Merge pull request #1 from leoz/master
Browse files Browse the repository at this point in the history
Crop image to circle
  • Loading branch information
benedom authored Jan 23, 2024
2 parents 7b9aa6b + c59a4cf commit d128aa7
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 13 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ xcuserdata/
/*.gcno
**/xcshareddata/WorkspaceSettings.xcsettings

### Visual Studio Code ###
.build

# End of https://www.toptal.com/developers/gitignore/api/macos,xcode
87 changes: 79 additions & 8 deletions Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
}

Expand Down
18 changes: 14 additions & 4 deletions Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
10 changes: 9 additions & 1 deletion Sources/SwiftyCrop/View/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ struct CropView: View {
Spacer()

Button {
onComplete(viewModel.crop(image))
onComplete(cropImage())
dismiss()
} label: {
Text("save_button", tableName: localizableTableName, bundle: .module)
Expand All @@ -120,6 +120,14 @@ struct CropView: View {
.background(.black)
}

private func cropImage() -> UIImage? {
if maskShape == .circle && configuration.cropImageCircular {
viewModel.cropToCircle(image)
} else {
viewModel.cropToSquare(image)
}
}

private struct MaskShapeView: View {
let maskShape: MaskShape

Expand Down

0 comments on commit d128aa7

Please sign in to comment.