BuySellAds.com

Our DNA is written in Objective-C
Jump

Square-Cropping Images

Christian asks:

How would you – most elegantly – crop out the center square of an image, preserving the aspect ratio and output the image with a given size?

That’s a good question. There is a “classic” and a “modern” method to achieving this.

All drawing on iOS uses the Core Graphics framework. The initials of the name of this framework can be seen in many low-level drawing functions prefixed with “CG”. This framework is also know as “Quartz”, that that is mostly a marketing name as you don’t see it mentioned in Apple’s documentation.

Quartz does not know about Objective-C, as all its functions are C-functions. Common prefixes group similar functions, like CGContextSetInterpolationQuality and CGContextDrawImage both manipulate a CGContext entity. Quartz also does not know about ARC, anything that you create there you also have to destroy.

To make it easier to us developers UIKit adds an easier to use higher level layer on top of Quartz that abstracts away many of the headaches. One example of such a convenience is how you create a bitmap context to draw into. Orders of magnitude less code is needed via UIKit, than you would need with Quartz.

Bitmap Contexts

The “most elegantly” part of the question means that we won’t even bother with Quartz. Granted, there IS a method CGImageCreateWithImageInRect that would allow us to create a new CGImage from a smaller rectangle inside a source CGImage. UIImage is just a wrapper around CGImage.

In this case this method does not help us since we also have to scale the output image. Any time you wind up with a different size of image you have to employ a bitmap context. Graphics contexts are the basic canvas to draw onto. Bitmap contexts are a specialized variant of these optimized to create images from.

The usual steps are:

  1. Create a bitmap context fitting your output requirements
  2. Draw into the context, compose images on top of each other, knock yourself out
  3. Create a new image from the bitmap context
  4. Profit!

Granted you could use the CreateWithImageInRect first to cut out the center piece and then resize the output. But the resizing step would still require a bitmap context. The best way I know is to combine the two operations inside step 2 of the above list.

The code for this blog post is demonstrated in the SquareCrop example on my GitHub Examples repository.

I like to create a function for grouping together multiple operations that do not need any access to instance variables or methods of the class where I am using it. This allows me to move such a function to a more general place if I find that I would like to use it elsewhere as well. This pattern also communicates at a glance (to pros) that there exists no such dependency.

Square-Cropping Function

Here’s the content of my square-cropping function as you find it in the sample’s ViewController.m where it is used to crop an enclosed sample image. I added comments for every line to explain.

UIImage *squareCropImageToSideLength(UIImage *sourceImage,
                                     CGFloat sideLength)
{
   // input size comes from image
   CGSize inputSize = sourceImage.size;
 
   // round up side length to avoid fractional output size
   sideLength = ceilf(sideLength);
 
   // output size has sideLength for both dimensions
   CGSize outputSize = CGSizeMake(sideLength, sideLength);
 
   // calculate scale so that smaller dimension fits sideLength
   CGFloat scale = MAX(sideLength / inputSize.width,
                       sideLength / inputSize.height);
 
   // scaling the image with this scale results in this output size
   CGSize scaledInputSize = CGSizeMake(inputSize.width * scale,
                                       inputSize.height * scale);
 
   // determine point in center of "canvas"
   CGPoint center = CGPointMake(outputSize.width/2.0,
                                outputSize.height/2.0);
 
   // calculate drawing rect relative to output Size
   CGRect outputRect = CGRectMake(center.x - scaledInputSize.width/2.0,
                                  center.y - scaledInputSize.height/2.0,
                                  scaledInputSize.width,
                                  scaledInputSize.height);
 
   // begin a new bitmap context, scale 0 takes display scale
   UIGraphicsBeginImageContextWithOptions(outputSize, YES, 0);
 
   // optional: set the interpolation quality.
   // For this you need to grab the underlying CGContext
   CGContextRef ctx = UIGraphicsGetCurrentContext();
   CGContextSetInterpolationQuality(ctx, kCGInterpolationHigh);
 
   // draw the source image into the calculated rect
   [sourceImage drawInRect:outputRect];
 
   // create new image from bitmap context
   UIImage *outImage = UIGraphicsGetImageFromCurrentImageContext();
 
   // clean up
   UIGraphicsEndImageContext();
 
   // pass back new image
   return outImage;
}

This code uses UIKit drawing for everything with one exception. To set the interpolation quality you have to grab the underlying CGContext (which UIKit manages internally for us) and use the C-function to set the interpolation quality.

Another reminder of Quartz can be seen by the fact that we have to call UIGraphicsEndImageContext before the end of the function. This causes UIKit to free up the memory of the CGContext it created for us.

This function also automatically uses the device’s display scale by specifying scale 0 in UIGraphicsBeginImageContextWithOptions. With this it gets the appropriate scale directly from iOS without us having to worry about it. You should never hard-code the scale because maybe one day Apple might ship devices that have a different scale than 2 (for the current Retina devices).

7 lines of code do some necessary calculations, 4 lines do the actual drawing and 2 lines are demonstrating directly tweaking a setting on the graphics context.

The sample demonstrates the use of this function:

- (void)viewDidLoad
{
   [super viewDidLoad];
 
   UIImage *image = [UIImage imageNamed:@"Image.jpg"];
   UIImage *squareImage = squareCropImageToSideLength(image, 300);
   self.imageView.image = squareImage;
}

Note that the 300 here are points, not pixels. The actual mapping from points to pixels should always remain the task of the operating system.

Conclusion

Nowadays there is very little reason to drop down to the Quartz-level for simple drawing operations. I can only think of one good reason to stick to Quartz: platform independence. You could – conceivably – write complex drawing operations as Quartz code and thus reuse it for both iOS and OS X.

The key takeaway of this post is that if you want a different output size than your source image you have to work with a interim bitmap context. This is the canvas to compose your graphics into before turning them into new UIImages.

Please don’t just copy/paste code you find on Stack Overflow into your apps. It will make your life much easier if you understand the distinctions I elaborated on in this article and if you stick with UIKit drawing where you can.


Categories: Q&A

%d bloggers like this: