Just 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 resizer. As this app deals with full size photos then I need to resize those photos for avoiding excessive memory usage. Apple’s CoreGraphics framework provides APIs what are compatible with all the before mentioned platforms. Therefore, let’s revisit the image scaler created in Resizing UIImages with aspect fill on iOS and let’s refactor it to use purely multiplatform supported CGImages.
We’ll skip the part which discusses calculating a rectangle in the original image which is then resized for assuring the image is resized equally on both axes. It is important because the aspect ratio of the original image most of the time does not equal to the aspect ratio of the target size. Let’s now take a look at cropping and resizing the original image. Cropping is easy to do because CGImage already has a convenient cropping(to:) method. On the other hand, resizing requires setting up a CGContext with the same parameters as the cropped image but the pixel size must be set to the target size. When we now draw the cropped image to the context, then CoreGraphics takes care of resizing the image.
This file contains hidden or 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
I have created a multiplatform sample app which just has a single SwiftUI view shared with iOS and macOS (it is not a Mac Catalyst app). Because we use CGImages then all the code can be shared. The view just loads a full size photo, scales it, and displays it. In the example view we have resized the image by taking account the current displayScale. Depending on the displayScale value we need to make the pixel size of the CGImage larger by the factor of the displayScale. For example, if the point size is 200×200 points, displayScale is 3.0, then the pixel size of the CGImage needs to be 600×600. This will give us a nice and crisp end result when the image is rendered.
This file contains hidden or 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
CoreGraphics is available on multiple Apple platforms and it provides tools for cropping and resizing images. With just a little bit of refactoring we have turned UIImage based image scaler into CoreGraphics backed scaler which can be used on multiple platforms.
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.
This file contains hidden or 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
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.
This file contains hidden or 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
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:
This file contains hidden or 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
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.
This file contains hidden or 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
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.
This file contains hidden or 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
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:
This file contains hidden or 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