Convert between image (pixels) and scaleAspectFit UIImageView coordinates like a champ!

When working with images in iOS, it's sometimes necessary to convert between an image's pixel coordinates and the coordinates of the UIImageView that displays the image. Here's how to do it.

Convert between image (pixels) and scaleAspectFit UIImageView coordinates like a champ!
So, where's our pixel? - Photo by Annie Spratt / Unsplash

When working with images in iOS, it's sometimes necessary to convert between an image's pixel coordinates and the coordinates of the UIImageView that displays the image. For example, you might want to know the pixel coordinates of a specific point in the image that the user tapped on, or you might want to highlight a specific area of the image by drawing a rectangle in the UIImageView.

Having a few years of iOS development experience, I thought this would be a somewhat complex matter (ever heard of image scaling? Well, have a look) that I shouldn't just wing it if I didn't want it to come bite me in the buttocks later on, and that there had to be some pretty standard way to deal with it. Oh sweet summer child...

While I did find a library, it is archived and has been abandonned for two years. And the first StackOverflow answer doesn't account for differences in ratios between the image and it's containing UIImageView when using .scaleAspectFit scaling. This self-answer had a nice idea, namely using AVMakeRect to calculate the rect that the image would take up if it were scaled to fit within the UIImageView's bounds while maintaining its aspect ratio. But it could be improved and made both more generic (and thus reusable), and clearer.

So here's what I came up with.

Solution

We can break it down into three steps:

  1. Figure out the image's scaling factor
  2. Figure out the image's clipping, to calculate the image's origin x and y offsets
  3. Using said offsets and scaling factory, determine the point's pixel coordinates in the image

Implementation

Detect the image's scaling factor

This function calculates the scale factor by comparing the aspect ratios of the image and the UIImageView. It uses the AVMakeRect function to calculate the rect that the image would take up if it were scaled to fit within the UIImageView's bounds while maintaining its aspect ratio. It then compares the width and height of the image to the width and height of this rect to determine the scale factor.

func scale(for image: UIImage, in imageView: UIImageView) -> CGFloat {
    let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
    let scale = (
        x: image.size.width / imageView.bounds.width,
        y: image.size.height / imageView.bounds.height
    )
    return scale.x > scale.y ? scale.x : scale.y
}

Detect the image's clipping

This function calculates the rect that the image is clipped to in the UIImageView.

It first checks that the UIImageView has an image set, and if not, it returns the bounds of the UIImageView

It also checks that the content mode of the UIImageView is set to scaleAspectFit, which is the default content mode for UIImageView. If this is not the case, then the image is not being clipped and the function returns the bounds of the UIImageView.

If the conditions are met, then the function uses the AVMakeRect function to calculate the rect that the image would take up if it were scaled to fit within the UIImageView's bounds while maintaining its aspect ratio. This rect represents the area of the image that is visible in the UIImageView.

func contentClippingRect(for imageView: UIImageView) -> CGRect {
    guard let image = imageView.image else { return bounds }
    guard imageView.contentMode == .scaleAspectFit else { return bounds }
    return AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
}

Calculating the point's coordinate in the image, in pixels

This function takes in a point in the UIImageView's coordinate system and a reference to the UIImageView itself, and returns a point in the image's pixel coordinate system.

First, it checks that the reference view has an image set, and if not, it returns nil. Next, it uses the contentClippingRect function to get the rect that the image is clipped to in the UIImageView. The scale function is then used to calculate the scale factor that is used to convert the point from the UIImageView's coordinate system to the image's pixel coordinate system. The point is then returned with the x and y values multiplied by the scale factor.

func coordinatesInImageForPoint(point: CGPoint, from referenceView: UIImageView) -> CGPoint? {
    guard let image = referenceView.image else {
        return nil
    }
    let imageRect = contentClippingRect(for: referenceView)
    let scale = scale(for: image, in: referenceView)
    return CGPoint(
        x: (point.x  - imageRect.origin.x) * scale,
        y: (point.y - imageRect.origin.y) * scale
    )
}

All together

func coordinatesInImageForPoint(point: CGPoint, from referenceView: UIImageView) -> CGPoint? {
    guard let image = referenceView.image else {
        return nil
    }
    let imageRect = contentClippingRect(for: referenceView)
    let scale = scale(for: image, in: referenceView)
    return CGPoint(
        x: (point.x  - imageRect.origin.x) * scale,
        y: (point.y - imageRect.origin.y) * scale
    )
}

func scale(for image: UIImage, in imageView: UIImageView) -> CGFloat {
    let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
    let scale = (
        x: image.size.width / imageView.bounds.width,
        y: image.size.height / imageView.bounds.height
    )
    return scale.x > scale.y ? scale.x : scale.y
}

func contentClippingRect(for imageView: UIImageView) -> CGRect {
    guard let image = imageView.image else { return bounds }
    guard imageView.contentMode == .scaleAspectFit else { return bounds }
    return AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)
}

Conclusion

And that's it folks! With these three handy functions, you'll be able to convert between image pixels and UIImageView coordinates like a pro. Which is basically just mapping between different coordinate systems, which should be easy, right? Oh, well, nevermind. I hope it helps, and, as usual, I wish you all a great day!

The swimming pool I'm not in currently, because instead of enjoying life I'm writing this, and working, and there's too much sun now anyway... wait. What am I complaining about? - Photo by Jay Solomon / Unsplash