2
votes

I'm sure this has been asked a number of times from various different perspectives, but I'm unable to find an answer on here as yet.

What I want to achieve What I would like to do is to display a UIImage, and allow the user to draw a rectangle on the image, and eventually crop their selection.

Research so far I've found previous questions here on SO that handle the cropping, however they often deal with static cropping areas that don't change, this does lead to the following constraints of such mechanism

  1. The area of the image you're interested in may be positioned anywhere, for example if you're trying to crop a road sign, it may be centered on one image, but aligned left on another, therefore you can't predict which area to crop.
  2. The size and scale of the interested area may change, for example one image may be a close up of that road sign so the cropping area would be larger, but another image may have been taken from a distance meaning the cropping area would be smaller.

With the combination of the above two variables, its almost impossible to accurately predict where the area of interest in the image would be, so I'm relying on the user to define this by being able to "draw" a box around the area we're interested in, in this case, a road sign.

This is all peachy on Android, since you can delegate all the hard work out with a nice intent, such as :

Intent intent = new Intent("com.android.camera.action.CROP");

However, I can't find an equivalent for iOS.

I've found this bit of code from this source :

- (UIImage *)imageByDrawingCircleOnImage:(UIImage *)image
{
    // begin a graphics context of sufficient size
    UIGraphicsBeginImageContext(image.size);

    // draw original image into the context
    [image drawAtPoint:CGPointZero];

    // get the context for CoreGraphics
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // set stroking color and draw circle
    [[UIColor redColor] setStroke];

    // make circle rect 5 px from border
    CGRect circleRect = CGRectMake(0, 0,
                image.size.width,
                image.size.height);
    circleRect = CGRectInset(circleRect, 5, 5);

    // draw circle
    CGContextStrokeEllipseInRect(ctx, circleRect);

    // make image out of bitmap context
    UIImage *retImage = UIGraphicsGetImageFromCurrentImageContext();

    // free the context
    UIGraphicsEndImageContext();

    return retImage;
}

Which I believe is a good starting point for cropping (it does crop circles however), it does rely on predefining the area you want to crop when calling CGRectMake.

This previous question also details how to do the actual cropping.

I'm assuming that to allow the user to draw the rect, I'd need to integrate with Gestures?

The Question : How can I allow the user, to draw a rect over an image view, with the intent of cropping that area?

2
I guess it would be much much easier to go vith extra UIView added as a subview of your UIImageView - or simply on top of it. User would be resizing this UIView and when ready to crop you just use its dimensions.Rok Jarc

2 Answers

2
votes

You could give BJImageCropper a try:

A simple UIView subclass that allows a user to crop an image. If you use it, I'd love to know! Twitter: @barrettjacobsen

0
votes

This post is already 5 years old, but future references, this is how I managed to get it done. Following code is a combination of Rob's answer and some image cropping

Xcode 9 and Swift 4 are being used here

  1. Add 2 ViewControllers

Add ImageView and 2 buttons for first view controller and another image view for last view controller

enter image description here

link all views to source file

  1. View controller

    import UIKit
    
    extension UIView {
    func snapshot(afterScreenUpdates: Bool = false) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
    drawHierarchy(in: bounds, afterScreenUpdates: afterScreenUpdates)
    let image = UIGraphicsGetImageFromCurrentImageContext()!
    UIGraphicsEndImageContext()
    return image
    }
    }
    
    extension UIImage {
    func crop( rect: CGRect) -> UIImage {
    var rect = rect
    rect.origin.x*=self.scale
    rect.origin.y*=self.scale
    rect.size.width*=self.scale
    rect.size.height*=self.scale
    
    let imageRef = self.cgImage!.cropping(to: rect)
    let image = UIImage(cgImage: imageRef!, scale: self.scale, orientation: self.imageOrientation)
    return image
    }
    }
    

class ViewController: UIViewController {

var rec: CGRect!
var cropImage: UIImage!
@IBOutlet weak var imageView: UIImageView!

private let shapeLayer: CAShapeLayer = {
    let _shapeLayer = CAShapeLayer()
    _shapeLayer.fillColor = UIColor.clear.cgColor
    _shapeLayer.strokeColor = UIColor.green.cgColor
    _shapeLayer.lineWidth = 2
    return _shapeLayer
}()

private var startPoint: CGPoint!

override func viewDidLoad() {
    super.viewDidLoad()
    imageView.layer.addSublayer(shapeLayer)
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    clear()
    startPoint = touches.first?.location(in: imageView)
}

func clear() {
    imageView.layer.sublayers = nil
    imageView.image = UIImage(named: "aa")
    imageView.layer.addSublayer(shapeLayer)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let startPoint = startPoint, let touch = touches.first else { return }
    let point: CGPoint

    if let predictedTouch = event?.predictedTouches(for: touch)?.last {
        point = predictedTouch.location(in: imageView)
    } else {
        point = touch.location(in: imageView)
    }
    updatePath(from: startPoint, to: point)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let startPoint = startPoint, let touch = touches.first else { return }
    let point = touch.location(in: imageView)
    updatePath(from: startPoint, to: point)
    imageView.image = imageView.snapshot(afterScreenUpdates: true)
    shapeLayer.path = nil
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    shapeLayer.path = nil
}

private func updatePath(from startPoint: CGPoint, to point: CGPoint) {
    let size = CGSize(width: point.x - startPoint.x, height: point.y - startPoint.y)
    rec = CGRect(origin: startPoint, size: size)
    shapeLayer.path = UIBezierPath(rect: rec).cgPath
}
@IBAction func btnTapped(_ sender: Any) {
    clear()
}

@IBAction func btnCropTapped(_ sender: Any) {
    let newRec = CGRect(origin: CGPoint(x: rec.origin.x, y: rec.origin.y), size: rec.size)
    cropImage = imageView.image?.crop(rect: newRec)
    print(rec)
    print(newRec)
    self.performSegue(withIdentifier: "toImage", sender: nil)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "toImage" {
        if let destination = segue.destination as? ImageViewController {
            destination.croppedImage = cropImage
        }
    }
}

}