Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Rectangle (4:3) for cropping #17

Merged
merged 3 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ If you want to display `SwiftyCrop` inside a sheet, use `NavigationView` instead
SwiftyCrop supports two different mask shapes for cropping:
- `circle`
- `square`
- `rectangle`

This is only the shape of the mask the user will see when cropping the image. The resulting, cropped image will always be a square by default. You can override this using a configuration.

Expand Down
87 changes: 85 additions & 2 deletions Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ class CropViewModel: ObservableObject {
private let maxMagnificationScale: CGFloat
var imageSizeInView: CGSize = .zero {
didSet {
maskRadius = min(maskRadius, min(imageSizeInView.width, imageSizeInView.height) / 2)
if maskShape == .rectangle {
let maxWidthForAspectRatio = (imageSizeInView.height * 4) / 3
maskRadius = min(maskRadius, min(maxWidthForAspectRatio, imageSizeInView.width) / 2)
} else {
maskRadius = min(maskRadius, min(imageSizeInView.width, imageSizeInView.height) / 2)
}
}
}
private let maskShape: MaskShape

@Published var maskRadius: CGFloat

@Published var scale: CGFloat = 1.0
Expand All @@ -19,10 +26,12 @@ class CropViewModel: ObservableObject {

init(
maskRadius: CGFloat,
maxMagnificationScale: CGFloat
maxMagnificationScale: CGFloat,
maskShape: MaskShape
) {
self.maskRadius = maskRadius
self.maxMagnificationScale = maxMagnificationScale
self.maskShape = maskShape
}

/**
Expand All @@ -34,6 +43,17 @@ class CropViewModel: ObservableObject {
let xLimit = ((imageSizeInView.width / 2) * scale) - maskRadius
return CGPoint(x: xLimit, y: yLimit)
}
func calculateDragGestureMaxRectangle() -> CGPoint {
Festanny marked this conversation as resolved.
Show resolved Hide resolved
// Calculate the width and height limits for 4:3 aspect ratio
let aspectRatio: CGFloat = 4 / 3

// Calculate the limits based on the imageSizeInView
let yLimit = ((imageSizeInView.height / 2) * scale) - (maskRadius * aspectRatio)
let xLimit = ((imageSizeInView.width / 2) * scale) - maskRadius

return CGPoint(x: xLimit, y: yLimit)
}


/**
Calculates the maximum magnification values that are applied when zooming the image,
Expand All @@ -46,6 +66,27 @@ class CropViewModel: ObservableObject {
let minScale = (maskRadius * 2) / min(imageSizeInView.width, imageSizeInView.height)
return (minScale, maxMagnificationScale)
}

/**
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 cropToRectangle(_ image: UIImage) -> UIImage? {
guard let orientedImage = image.correctlyOriented else {
return nil
}

let cropRect = calculateCropRectRectangle(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 square.
Expand Down Expand Up @@ -166,6 +207,48 @@ class CropViewModel: ObservableObject {

return UIImage(cgImage: result)
}

/**
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 calculateCropRectRectangle(_ orientedImage: UIImage) -> CGRect {
// Aspect ratio 4:3
let aspectRatio: CGFloat = 4 / 3
// The ratio factor of the original image to the displayed one
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
)
// Calculating the cropping radius for the width, taking into account the aspect ratio
let cropWidthRadiusInOriginalImage = (maskRadius * factor) / scale
let cropHeightRadiusInOriginalImage = cropWidthRadiusInOriginalImage * aspectRatio
// Image offsets along the x and y axes when dragging
let offsetX = offset.width * factor
let offsetY = offset.height * factor
// Calculating the coordinates of the cropping rectangle inside the original image
let cropRectX = (centerInOriginalImage.x - cropWidthRadiusInOriginalImage) - (offsetX / scale)
let cropRectY = (centerInOriginalImage.y - cropHeightRadiusInOriginalImage) - (offsetY / scale)
let cropRectCoordinate = CGPoint(x: cropRectX, y: cropRectY)
// Dimensions of the cropping rectangle, taking into account the aspect ratio
let cropRectWidth = cropWidthRadiusInOriginalImage * 2
let cropRectHeight = cropHeightRadiusInOriginalImage * 2

let cropRect = CGRect(
x: cropRectCoordinate.x,
y: cropRectCoordinate.y,
width: cropRectWidth,
height: cropRectHeight
)

return cropRect
}

/**
Calculates the rectangle to crop.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SwiftyCrop/Models/MaskShape.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
public enum MaskShape: CaseIterable {
case circle, square
case circle, square, rectangle
}
11 changes: 9 additions & 2 deletions Sources/SwiftyCrop/View/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ struct CropView: View {
_viewModel = StateObject(
wrappedValue: CropViewModel(
maskRadius: configuration.maskRadius,
maxMagnificationScale: configuration.maxMagnificationScale
maxMagnificationScale: configuration.maxMagnificationScale,
maskShape: maskShape
)
)
localizableTableName = "Localizable"
Expand All @@ -50,7 +51,7 @@ struct CropView: View {

let dragGesture = DragGesture()
.onChanged { value in
let maxOffsetPoint = viewModel.calculateDragGestureMax()
let maxOffsetPoint = maskShape == .rectangle ? viewModel.calculateDragGestureMaxRectangle() : viewModel.calculateDragGestureMax()
Festanny marked this conversation as resolved.
Show resolved Hide resolved
let newX = min(
max(value.translation.width + viewModel.lastOffset.width, -maxOffsetPoint.x),
maxOffsetPoint.x
Expand Down Expand Up @@ -149,6 +150,8 @@ struct CropView: View {
}
if configuration.cropImageCircular && maskShape == .circle {
return viewModel.cropToCircle(editedImage)
} else if maskShape == .rectangle {
return viewModel.cropToRectangle(editedImage)
} else {
return viewModel.cropToSquare(editedImage)
}
Expand All @@ -165,6 +168,10 @@ struct CropView: View {

case .square:
Rectangle()

case .rectangle:
Rectangle()
.aspectRatio(3/4, contentMode: .fill)
}
}
}
Expand Down