Resizing UIImages with aspect fill on iOS

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.

// Scaling the full image
let originalImage = UIImage(named: "example_photo.jpg")!
var targetSize = CGSize(width: 100, height: 100)
imageView.image = ImageScaler.scaleToFill(originalImage, in: targetSize)
// Scaling only a small part of the image
let originalImage = UIImage(named: "example_photo.jpg")!
var fromRect = CGRect(x: 362.0, y: 449.0, width: 260.0, height: 160.0)
var targetSize = CGSize(width: 100, height: 100)
imageView.image = ImageScaler.scaleToFill(originalImage, in: targetSize, from: fromRect)
view raw Image.swift hosted with ❤ by GitHub
Scaling an image to a target size.

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.

var transform = CGAffineTransform(translationX: 0.0, y: targetSize.height)
transform = transform.scaledBy(x: 1, y: -1)
context.cgContext.concatenate(transform)
view raw CGContext.swift hosted with ❤ by GitHub
Flipping the image vertically in the CGContext .

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:

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)
}
}
}
view raw Resize.swift hosted with ❤ by GitHub
Crop and resize the image.

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.

extension CGSize {
enum Aspect {
case portrait, landscape, square
}
var aspect: Aspect {
switch width / height {
case 1.0:
return .square
case 1.0:
return .landscape
default:
return .portrait
}
}
var aspectRatio: CGFloat {
return width / height
}
}
view raw CGSizeAspect.swift hosted with ❤ by GitHub
Convenience properties on CGRect.

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.

extension CGRect {
func scaled(toFillSize targetSize: CGSize) -> CGRect {
var scaledRect = self
switch (size.aspect, targetSize.aspect) {
case (.portrait, .portrait), (.portrait, .square):
scaledRect.size.height = width / targetSize.aspectRatio
scaledRect.size.width = width
if scaledRect.height > height {
scaledRect.size = size
}
scaledRect.origin.y -= (scaledRect.height height) / 2.0
case (.portrait, .landscape), (.square, .landscape):
scaledRect.size.height = width / targetSize.aspectRatio
scaledRect.size.width = width
if scaledRect.height > height {
scaledRect.size = size
}
scaledRect.origin.y -= (scaledRect.height height) / 2.0
case (.landscape, .portrait), (.square, .portrait):
scaledRect.size.height = height
scaledRect.size.width = height * targetSize.aspectRatio
if scaledRect.width > width {
scaledRect.size = size
}
scaledRect.origin.x -= (scaledRect.width width) / 2.0
case (.landscape, .landscape), (.landscape, .square):
scaledRect.size.height = height
scaledRect.size.width = height * targetSize.aspectRatio
if scaledRect.size.width > width {
scaledRect.size = size
}
scaledRect.origin.x -= (scaledRect.width width) / 2.0
case (.square, .square):
return self
}
return scaledRect.integral
}
}
Scaling rect to the target size with keeping the initial aspect ratio.

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:

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)
}
}
}
}

view raw
ImageScaler.swift
hosted with ❤ by GitHub

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 Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s