Resizing images is an important topic when we need to display images which do not match with the intended display size. For example, rendering much large images in a small rectangle. UIImageView supports scaling images automatically but that becomes inefficient when dealing with larger images. In this blog post we’ll take a look on how to crop and resize images to fill a target size while keeping the original aspect ratio.
Cropping and scaling UIImages
The end goal of this exercise is to create a small ImageScaler struct which supports cropping the original image and resizing it to the target size. The final instance of the UIImage has smaller size which means that less memory is required for rendering the image.
As the first step let’s take a look on how to write the cropping and scaling logic. NSHipster has a great post about the different techniques what we can use for resizing images. We are going to use UIGraphicsImageRenderer for creating the scaled image. One important thing to note is that when we use CGContext for drawing the image then we need to flip the coordinate system because UIImage’s and CGContext’s coordinates do not match (UIImage uses upper left corner, CGContext bottom left corner). Coordinates can be transformed from the UIImage coordinate system to the CGContext coordinate system by combining translation and scale transforms. First we’ll move the image and then flip it in the opposite direction so that the final frame stays in the image rect.
Next step after applying the affine transform is to crop the image. UIImage has a cgImage property but it can be nil when the instance was initialized with a CIImage backing storage. Therefore, we’ll need to handle both cases. Apple has a convenience drawing method for CIImage which already knows how to handle cropping. On the otherhand CGImage needs to be cropped first and then drawn. The full implementation of the crop and resize becomes:
Calculating rects for scaled to fill behavior
Now we have cropping and resize logic available. Depending on the target size of the image we’ll need to figure out which parts of the original image should be cropped so that the original aspect ratio does not change. Note that the original image size and the target size can have different aspect ratios: square, portrait, landscape. Therefore, we’ll need to handle all the cases. Let’s start by adding convenience properties to CGSize.
After that we can add a method on CGRect which calculates a CGRect of the original image, what can be drawn in the target image. There are 9 different combinations what we need to handle. But the core logic stays the same: scale the current rectangle so that it fills the target size while keeping the original aspect ratio. Then chop off the sides which go over the target size and center the image in the target size.
Finalizing the ImageScaler
We can proceed with creating a single static method which takes care of scaling the original image to the target size while keeping the aspect ratio. The whole implementation looks like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct ImageScaler { | |
static func scaleToFill(_ image: UIImage, in targetSize: CGSize, from fromRect: CGRect = .zero) -> UIImage { | |
let rect = fromRect.isEmpty ? CGRect(origin: .zero, size: image.size) : fromRect | |
let scaledRect = rect.scaled(toFillSize: targetSize) | |
return scale(image, fromRect: scaledRect, targetSize: targetSize) | |
} | |
private static func scale(_ image: UIImage, fromRect: CGRect = .zero, targetSize: CGSize) -> UIImage { | |
let renderer = UIGraphicsImageRenderer(size: targetSize) | |
return renderer.image { context in | |
// UIImage and CGContext coordinates are flipped. | |
var transform = CGAffineTransform(translationX: 0.0, y: targetSize.height) | |
transform = transform.scaledBy(x: 1, y: -1) | |
context.cgContext.concatenate(transform) | |
// UIImage -> cropped CGImage | |
let makeCroppedCGImage: (UIImage) -> CGImage? = { image in | |
guard let cgImage = image.cgImage else { return nil } | |
guard !fromRect.isEmpty else { return cgImage } | |
return cgImage.cropping(to: fromRect) | |
} | |
if let cgImage = makeCroppedCGImage(image) { | |
context.cgContext.draw(cgImage, in: CGRect(origin: .zero, size: targetSize)) | |
} | |
else if let ciImage = image.ciImage { | |
var transform = CGAffineTransform(translationX: 0.0, y: image.size.height) | |
transform = transform.scaledBy(x: 1, y: -1) | |
let adjustedFromRect = fromRect.applying(transform) | |
let ciContext = CIContext(cgContext: context.cgContext, options: nil) | |
ciContext.draw(ciImage, in: CGRect(origin: .zero, size: targetSize), from: adjustedFromRect) | |
} | |
} | |
} | |
} |
Summary
While UIImageView supports cropping and scaling images, we’ll need to do this on our own. It becomes handy especially when dealing with larger images.
If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.
2 replies on “Resizing UIImages with aspect fill on iOS”
[…] a couple of weeks ago I wrote about resizing images on iOS: Resizing UIImages with aspect fill on iOS. As I am currently building a macOS, iOS, watchOS app I realized that I need a multiplatform image […]
LikeLike
[…] Resizing UIImages with aspect fill on iOS (October 25, 2020) […]
LikeLike