14

I'm trying to crop a sub-image of a image view using an overlay UIView that can be positioned anywhere in the UIImageView. I'm borrowing a solution from a similar post on how to solve this when the UIImageView content mode is 'Aspect Fit'. That proposed solution is:

  func computeCropRect(for sourceFrame : CGRect) -> CGRect {

        let widthScale = bounds.size.width / image!.size.width
        let heightScale = bounds.size.height / image!.size.height

        var x : CGFloat = 0
        var y : CGFloat = 0
        var width : CGFloat = 0
        var height : CGFloat = 0
        var offSet : CGFloat = 0

        if widthScale < heightScale {
            offSet = (bounds.size.height - (image!.size.height * widthScale))/2
            x = sourceFrame.origin.x / widthScale
            y = (sourceFrame.origin.y - offSet) / widthScale
            width = sourceFrame.size.width / widthScale
            height = sourceFrame.size.height / widthScale
        } else {
            offSet = (bounds.size.width - (image!.size.width * heightScale))/2
            x = (sourceFrame.origin.x - offSet) / heightScale
            y = sourceFrame.origin.y / heightScale
            width = sourceFrame.size.width / heightScale
            height = sourceFrame.size.height / heightScale
        }

        return CGRect(x: x, y: y, width: width, height: height)
    }

The problem is that using this solution when the image view is aspect fill causes the cropped segment to not line up exactly with where the overlay UIView was positioned. I'm not quite sure how to adapt this code to accommodate for Aspect Fill or reposition my overlay UIView so that it lines up 1:1 with the segment I'm trying to crop.

UPDATE Solved using Matt's answer below

class ViewController: UIViewController {

    @IBOutlet weak var catImageView: UIImageView!
    private var cropView : CropView!

    override func viewDidLoad() {
        super.viewDidLoad()

        cropView = CropView(frame: CGRect(x: 0, y: 0, width: 45, height: 45))

        catImageView.image = UIImage(named: "cat")
        catImageView.clipsToBounds = true

        catImageView.layer.borderColor = UIColor.purple.cgColor
        catImageView.layer.borderWidth = 2.0
        catImageView.backgroundColor = UIColor.yellow

        catImageView.addSubview(cropView)

        let imageSize = catImageView.image!.size
        let imageViewSize = catImageView.bounds.size

        var scale : CGFloat = imageViewSize.width / imageSize.width

        if imageSize.height * scale < imageViewSize.height {
            scale = imageViewSize.height / imageSize.height
        }

        let croppedImageSize = CGSize(width: imageViewSize.width/scale, height: imageViewSize.height/scale)

        let croppedImrect =
            CGRect(origin: CGPoint(x: (imageSize.width-croppedImageSize.width)/2.0,
                                   y: (imageSize.height-croppedImageSize.height)/2.0),
                   size: croppedImageSize)

        let renderer = UIGraphicsImageRenderer(size:croppedImageSize)

        let _ = renderer.image { _ in
            catImageView.image!.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
        }
    }


    @IBAction func performCrop(_ sender: Any) {
        let cropFrame = catImageView.computeCropRect(for: cropView.frame)
        if let imageRef = catImageView.image?.cgImage?.cropping(to: cropFrame) {
            catImageView.image = UIImage(cgImage: imageRef)
        }
    }

    @IBAction func resetCrop(_ sender: Any) {
        catImageView.image = UIImage(named: "cat")
    }
}

The Final Result enter image description here

zic10
  • 2,310
  • 5
  • 30
  • 55
  • But your new material shows a screen shot where the original image is not affected by `aspectFill` or `aspectFit`; the entire image is being displayed! I don't think your image view is configured the way you think it is. – matt May 01 '17 at 19:20
  • I double checked and both of my image views are set to `aspectFill`. It might be because I'm assigning the `aspectCroppedImage` to the `destinationImageView` directly? – zic10 May 01 '17 at 19:23
  • Going to update my implementation and screenshots real quick for clarity. – zic10 May 01 '17 at 19:25
  • Well then you're creating `aspectCroppedImage` at totally the wrong time. – matt May 01 '17 at 20:40
  • When should the code to compute this actually be called? – zic10 May 01 '17 at 20:46
  • If you don't understand my answer, unaccept it and I'll delete it. – matt May 01 '17 at 21:22
  • I had a few bugs in my implementation that were causing your solution to misbehave. I posted a gif and it looks like it's working as expected. Thanks for all your help! – zic10 May 02 '17 at 16:47

2 Answers2

24

Let's divide the problem into two parts:

  1. Given the size of a UIImageView and the size of its UIImage, if the UIImageView's content mode is Aspect Fill, what is the part of the UIImage that fits into the UIImageView? We need, in effect, to crop the original image to match what the UIImageView is actually displaying.

  2. Given an arbitrary rect within the UIImageView, what part of the cropped image (derived in part 1) does it correspond to?

The first part is the interesting part, so let's try it. (The second part will then turn out to be trivial.)

Here's the original image I'll use:

https://static1.squarespace.com/static/54e8ba93e4b07c3f655b452e/t/56c2a04520c64707756f4267/1455596221531/

That image is 1000x611. Here's what it looks like scaled down (but keep in mind that I'm going to be using the original image throughout):

enter image description here

My image view, however, will be 139x182, and is set to Aspect Fill. When it displays the image, it looks like this:

enter image description here

The problem we want to solve is: what part of the original image is being displayed in my image view, if my image view is set to Aspect Fill?

Here we go. Assume that iv is the image view:

let imsize = iv.image!.size
let ivsize = iv.bounds.size

var scale : CGFloat = ivsize.width / imsize.width
if imsize.height * scale < ivsize.height {
    scale = ivsize.height / imsize.height
}

let croppedImsize = CGSize(width:ivsize.width/scale, height:ivsize.height/scale)
let croppedImrect =
    CGRect(origin: CGPoint(x: (imsize.width-croppedImsize.width)/2.0,
                           y: (imsize.height-croppedImsize.height)/2.0),
           size: croppedImsize)

So now we have solved the problem: croppedImrect is the region of the original image that is showing in the image view. Let's proceed to use our knowledge, by actually cropping the image to a new image matching what is shown in the image view:

let r = UIGraphicsImageRenderer(size:croppedImsize)
let croppedIm = r.image { _ in
    iv.image!.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
}

The result is this image (ignore the gray border):

enter image description here

But lo and behold, that is the correct answer! I have extracted from the original image exactly the region portrayed in the interior of the image view.

So now you have all the information you need. croppedIm is the UIImage actually displayed in the clipped area of the image view. scale is the scale between the image view and that image. Therefore, you can easily solve the problem you originally proposed! Given any rectangle imposed upon the image view, in the image view's bounds coordinates, you simply apply the scale (i.e. divide all four of its attributes by scale) — and now you have the same rectangle as a portion of croppedIm.

(Observe that we didn't really need to crop the original image to get croppedIm; it was sufficient, in reality, to know how to perform that crop. The important information is the scale along with the origin of croppedImRect; given that information, you can take the rectangle imposed upon the image view, scale it, and offset it to get the desired rectangle of the original image.)


EDIT I added a little screencast just to show that my approach works as a proof of concept:

enter image description here

EDIT Also created a downloadable example project here:

https://github.com/mattneub/Programming-iOS-Book-Examples/blob/39cc800d18aa484d17c26ffcbab8bbe51c614573/bk2ch02p058cropImageView/Cropper/ViewController.swift

But note that I can't guarantee that URL will last forever, so please read the discussion above to understand the approach used.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    I understand what you have posted but could you elaborate on what you meant by "simply apply the scale and now you have the same rectangle as a portion of croppedIm". My crop view is dynamic in size and position and even using the croppedIm and using my function I supplied above the crop is still off and I'm not quite sure why. – zic10 May 01 '17 at 18:48
  • If your code works for aspect fit, it will work if you substitute `croppedIm` and use aspect fit. So why not just do that? That is what I meant by reducing it to a previously solved problem. – matt May 01 '17 at 18:58
  • Yeah I understand what you are saying. I'm going to update my original question with the code including your solution for context. – zic10 May 01 '17 at 18:59
  • I added a little screencast to show that my approach works correctly for actually cropping the image to a designated rect. – matt May 02 '17 at 01:02
  • Updated my post with workable solution and a gif demonstrating functionality. – zic10 May 02 '17 at 16:47
  • @matt just curious why you had to negate the values on the draw command. – Juan Boero Apr 23 '20 at 03:03
  • @JuanBoero Try not negating them and see. – matt Apr 23 '20 at 03:17
  • This example doesn't work properly when your image view is placed inside scroll view and translated/scaled. I took code from here https://www.raywenderlich.com/5758454-uiscrollview-tutorial-getting-started and tried to use your code to get image from a visible rect. The result is I get a cropped image but the original image view "jumps" at this moment – Vyachaslav Gerchicov Nov 16 '20 at 12:34
  • can we use `UIGraphisBeginImageContext` to crop instead of `UIGraphicsImageRenderer`, the latter seems block main thread. – Frank Sep 03 '21 at 02:34
  • @Frank And UIGraphicsBeginImageContext doesn't block the main thread? Drawing in a graphics context is drawing in a graphics context. – matt Sep 03 '21 at 02:46
  • @matt yes, I did run both approach from a background thread, `UIGraphicsImageRenderer` somehow still blocks main thread, maybe it runs in main thread internally. – Frank Sep 03 '21 at 03:31
  • @Frank well UIGraphicsBeginImageContext is fine to use. – matt Sep 03 '21 at 03:49
  • @matt yeah agreed, both way work, just in my case, I choose to work with graphic context . – Frank Sep 03 '21 at 04:00
1

Matt answered the question perfectly. I was creating a full-screen camera and had a need to make the final output match the full-screen preview. Offering here a compact extension of Matt's overall answer in Swift 5 for easy use by others. Recommend reading Matt's answer as it explains things very well.

extension UIImage {
    func cropToRect(rect: CGRect) -> UIImage? {
        var scale = rect.width / self.size.width
        scale = self.size.height * scale < rect.height ? rect.height/self.size.height : scale

        let croppedImsize = CGSize(width:rect.width/scale, height:rect.height/scale)
        let croppedImrect = CGRect(origin: CGPoint(x: (self.size.width-croppedImsize.width)/2.0,
                                                   y: (self.size.height-croppedImsize.height)/2.0),
                                                   size: croppedImsize)
        UIGraphicsBeginImageContextWithOptions(croppedImsize, true, 0)
        self.draw(at: CGPoint(x:-croppedImrect.origin.x, y:-croppedImrect.origin.y))
        let croppedImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return croppedImage
    }
}
C6Silver
  • 3,127
  • 2
  • 21
  • 49
  • it seems it doesn't work at all - I always get the same rect. No matter you pass other origin or other size - the result is always the same – Vyachaslav Gerchicov Nov 16 '20 at 12:48
  • I would look to answer your question, but instead of you just asking it, you choose to downvote a viable solution that works because it may not fit your specific use case or errors. Good luck to you. – C6Silver Nov 16 '20 at 20:31
  • Yes, maybe my code contains errors/bugs but anyways the confirmed answer works, your one dosn't. – Vyachaslav Gerchicov Nov 17 '20 at 07:30