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

Rotate image #4

Merged
merged 10 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion Demo/SwiftyCropDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -56,6 +57,7 @@ struct ContentView: View {
)
}
Toggle("Crop image to circle", isOn: $cropImageCircular)
Toggle("Rotate image", isOn: $rotateImage)
}
.buttonStyle(.bordered)
.padding()
Expand All @@ -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
Expand Down
64 changes: 63 additions & 1 deletion Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
}
7 changes: 6 additions & 1 deletion Sources/SwiftyCrop/Models/SwiftyCropConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
Expand All @@ -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
}
}
100 changes: 60 additions & 40 deletions Sources/SwiftyCrop/View/CropView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -41,6 +84,7 @@ struct CropView: View {
Image(uiImage: image)
.resizable()
.scaledToFit()
.rotationEffect(viewModel.angle)
.scaleEffect(viewModel.scale)
.offset(viewModel.offset)
.opacity(0.5)
Expand All @@ -56,6 +100,7 @@ struct CropView: View {
Image(uiImage: image)
.resizable()
.scaledToFit()
.rotationEffect(viewModel.angle)
.scaleEffect(viewModel.scale)
.offset(viewModel.offset)
.mask(
Expand All @@ -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 {
Expand All @@ -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)
}
}

Expand Down
Loading