Earlier this year, I blogged about a small extension I wrote for UIImage for resizing images within a bounding square. Part of my current project, a rewrite of my Nodality application in Swift, requires similar functionality but I wanted to improve the scaling quality and handle images with different orientations (i.e. portrait and landscape).
My resizeToBoundingSquare() function accepts an image and numeric value that defines the bounding square's width and it returns a UIImage. The signature, therefore, is:
func resizeToBoundingSquare(sourceImage: UIImage, #boundingSquareSideLength : CGFloat) -> UIImage
My first step was to use Lanczos resampling rather than simply relying on drawInRect() at the new size. Lanczos offers smooth resampling which should lead to a better quality image and it's available as one of the built in Core Image filters.
The code to execute the filter and generate a properly sized CIImage is:
let imgScale = sourceImage.size.width< sourceImage.size.height ? boundingSquareSideLength / sourceImage.size.width : boundingSquareSideLength / sourceImage.size.height
let scaleTransform = CIFilter(name: "CILanczosScaleTransform")
scaleTransform.setValue(CIImage(image: sourceImage), forKey: "inputImage")
scaleTransform.setValue(imgScale, forKey: "inputScale")
scaleTransform.setValue(1.0, forKey: "inputAspectRatio")
let outputImage = scaleTransform.valueForKey("outputImage") asCIImage
The CIImage is scaled but not square. To create a cropped and centred UIImage, I use the following code:
let context = CIContext(options: nil)
let extent = outputImage.extent()
let xOffset = Int(extent.width) > NodeConstants.imageWidth ? (extent.width- boundingSquareSideLength) /2 : 0
let yOffset = Int(extent.height) > NodeConstants.imageWidth ? (extent.height- boundingSquareSideLength) /2 : 0
let scaledImage = UIImage(CGImage: context.createCGImage(outputImage, fromRect: CGRect(x: xOffset, y: yOffset, width: boundingSquareSideLength, height: boundingSquareSideLength)))!
Now we have a scaled, square UIImage but there's one final issue. If the original source has been shot in landscape format, the image is rotated. So the final steps are to look at the source image's orientation and rotate accordingly. First, I define an angle to rotate by:
var angle = 0.0;
if (sourceImage.imageOrientation==UIImageOrientation.Right)
{
angle = 90.0
}
elseif (sourceImage.imageOrientation==UIImageOrientation.Left)
{
angle = -90.0
}
elseif (sourceImage.imageOrientation==UIImageOrientation.Down)
{
angle = 180
}
elseif (sourceImage.imageOrientation==UIImageOrientation.Up)
{
angle = 0.0
}
Then I use a CGContext to draw the image. Because the rotation pivot point is at (0,0), I need to translate the context by half the bounding square width before rotating and do the drawAtPoint() at the opposite of that translation:
UIGraphicsBeginImageContext(CGSize(width: boundingSquareSideLength, height: boundingSquareSideLength))
let cgContext = UIGraphicsGetCurrentContext()
let offset = boundingSquareSideLength /2.0
CGContextTranslateCTM(cgContext, offset, offset)
CGContextRotateCTM(cgContext, CGFloat(angle * M_PI / 180))
scaledImage.drawAtPoint(CGPoint(x: -offset, y: -offset))
Finally, I get the UIImage from the context and release it:
let final = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return final
And the result is beautifully scaled images, centred and cropped to a square!
This source code isn't publicly available on GitHub, but this post does contain the full source code of resizeToBoundingSquare().
Finally, if you're wondering how Nodality is looking in Swift, here's a little demo video to whet your appetite: