-
-
Save erica/157e20ea0c7e9f28a03a8b12448c8fd0 to your computer and use it in GitHub Desktop.
| import UIKit | |
| // Swift rewrite challenge | |
| // Starting point: https://gist.github.com/jkereako/200342b66b5416fd715a#file-scale-and-crop-image-swift | |
| func scaleAndCropImage( | |
| image: UIImage, | |
| toSize size: CGSize, | |
| fitImage: Bool = true | |
| ) -> UIImage { | |
| // Return original when cropping is not needed | |
| guard !CGSizeEqualToSize(image.size, size) else { return image } | |
| // Calculate scale factor for fit or fill | |
| let (widthFactor, heightFactor) = (size.width / image.size.width, size.height / image.size.height) | |
| let fitFillTest = fitImage ? widthFactor < heightFactor : widthFactor > heightFactor | |
| let scaleFactor = fitFillTest ? widthFactor : heightFactor | |
| // Establish drawing destination, which may start outside the drawing context bounds | |
| let (scaledWidth, scaledHeight) = (image.size.width * scaleFactor, image.size.height * scaleFactor) | |
| let drawingOrigin = CGPoint( | |
| x: (size.width - scaledWidth) / 2.0, | |
| y: (size.height - scaledHeight) / 2.0) | |
| // Perform drawing and return image | |
| UIGraphicsBeginImageContextWithOptions(size, false, 0.0) | |
| let scaledImage: UIImage | |
| do { | |
| // Fill background | |
| UIColor.blackColor().setFill(); UIRectFill(CGRect(origin: .zero, size: size)) | |
| // Draw scaled image | |
| let drawingRect: CGRect = CGRect( | |
| origin: drawingOrigin, | |
| size: CGSize(width: scaledWidth, height: scaledHeight)) | |
| image.drawInRect(drawingRect) | |
| // Fetch image | |
| scaledImage = UIGraphicsGetImageFromCurrentImageContext()! | |
| } | |
| UIGraphicsEndImageContext() | |
| return scaledImage | |
| } | |
| // Test with some basic placeholder data | |
| guard let url = NSURL(string: "http://placehold.it/300x150") else { fatalError("Bad URL") } | |
| guard let data = NSData(contentsOfURL: url) else { fatalError("Bad data") } | |
| guard let img = UIImage(data: data) else { fatalError("Bad data") } | |
| let outImageFit = scaleAndCropImage(img, toSize: CGSize(width: 200, height: 200)) | |
| let outImageFill = scaleAndCropImage(img, toSize: CGSize(width: 200, height: 200), fitImage: false) |
Updated with feedback:
func * (lhs: CGSize, rhs: CGFloat) -> CGSize { return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) }
func scaleAndCropImage(
image: UIImage,
toSize size: CGSize,
fitImage: Bool = true
) -> UIImage {
// Return original when cropping is not needed
guard image.size != size else { return image }
// Calculate scale factor for fit or fill
let scaleFactor = (fitImage ? min : max)(size.width / image.size.width, size.height / image.size.height)
// Establish drawing destination, which may start outside the drawing context bounds
let scaledSize = image.size * scaleFactor
let drawingOrigin = CGPoint(
x: (size.width - scaledSize.width) / 2.0,
y: (size.height - scaledSize.height) / 2.0)
let drawingRect = CGRect(origin: drawingOrigin, size: scaledSize)
// Perform drawing and return image
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let scaledImage: UIImage
do {
// Fill background
UIColor.blackColor().setFill(); UIRectFill(CGRect(origin: .zero, size: size))
// Draw scaled image
image.drawInRect(drawingRect)
// Fetch image
scaledImage = UIGraphicsGetImageFromCurrentImageContext()!
}
UIGraphicsEndImageContext()
return scaledImage
}or:
func * (lhs: CGSize, rhs: CGFloat) -> CGSize { return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) }
extension UIImage {
func scale(to destSize: CGSize, fitImage: Bool = true, backgroundColor: UIColor = .blackColor()) -> UIImage {
assert(destSize.width > 1.0 && destSize.height > 1.0, "Must scale to at least 1x1 point destination")
guard size != destSize else { return self }
// Calculate scale factor for fit or fill
let scaleFactor = (fitImage ? min : max)(destSize.width / size.width, destSize.height / size.height)
// Establish drawing destination, which may start outside the drawing context bounds
let scaledSize = size * scaleFactor
let drawingOrigin = CGPoint(
x: (destSize.width - scaledSize.width) / 2.0,
y: (destSize.height - scaledSize.height) / 2.0)
let drawingRect = CGRect(origin: drawingOrigin, size: scaledSize)
// Perform drawing and return image
UIGraphicsBeginImageContextWithOptions(destSize, false, 0.0); defer { UIGraphicsEndImageContext() }
backgroundColor.setFill(); UIRectFill(CGRect(origin: .zero, size: destSize))
self.drawInRect(drawingRect)
return UIGraphicsGetImageFromCurrentImageContext()
}
}or:
func * (lhs: CGSize, rhs: CGFloat) -> CGSize { return CGSize(width: lhs.width * rhs, height: lhs.height * rhs) }
extension UIImage {
func scale(to destSize: CGSize, fitImage: Bool = true, backgroundColor: UIColor = .blackColor()) -> UIImage {
assert(destSize.width > 1.0 && destSize.height > 1.0, "Must scale to at least 1x1 point destination")
guard size != destSize else { return self }
// Establish drawing destination, which may start outside the drawing context bounds
let scaleFactor = (fitImage ? min : max)(destSize.width / size.width, destSize.height / size.height)
let scaledSize = size * scaleFactor
let drawingRect = CGRect(origin: .zero, size: scaledSize)
.offsetBy(dx: (destSize.width - scaledSize.width) / 2.0,
dy: (destSize.height - scaledSize.height) / 2.0)
// Perform drawing and return image
UIGraphicsBeginImageContextWithOptions(destSize, false, 0.0); defer { UIGraphicsEndImageContext() }
backgroundColor.setFill(); UIRectFill(CGRect(origin: .zero, size: destSize))
self.drawInRect(drawingRect)
return UIGraphicsGetImageFromCurrentImageContext()
}
}I did not have a guard to prevent needless scaling in my version of this code. Good Stuff!
But I think you will like this defer : (had not refreshed the page to see your new comments...)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0.0)
defer { UIGraphicsEndImageContext() }
And maybe this bit :
func scale(toSize newSize: CGSize, fit: Bool) -> CGSize {
let test : (CGFloat,CGFloat) -> CGFloat = fit ? { max($0, $1) } : { min($0, $1) }
let scale : CGFloat = test(width / newSize.width, height / newSize.height)
return CGSize(width: (width / scale), height: (height / scale))
}
I would recommend to split into several functions (size calculations, image cropping, glue) for unit testing support.
Add asserts for size.width and size.height > 0.0 or some minimum point size as desired. See: http://twitter.com/deadbeefa/status/737722278608142345
Enjoyed the light challenge.
Here's my full rewrite (FWIW), it doesn't add the functionality you added.
I like your use of do and tuples.
My preference is for an extension of UIImage.
I still maintained an if/else/else.
also re: "http://placehold.it/300x150" … i heard angels sing (new to me and quite useful!)
extension UIImage {
func scaleAndCrop(size: CGSize) -> UIImage {
guard CGSizeEqualToSize(self.size, size) == false else {
return self
}
let widthFactor = size.width / self.size.width
let heightFactor = size.height / self.size.height
let scaleFactor = max(widthFactor, heightFactor)
let scaledWidth = self.size.width * scaleFactor
let scaledHeight = self.size.height * scaleFactor
var scaledRect: CGRect
if widthFactor > heightFactor {
scaledRect = CGRect(x: 0.0, y: (size.height - scaledHeight) / 2.0,
width: scaledWidth, height: scaledHeight)
} else if widthFactor < heightFactor {
scaledRect = CGRect(x: (size.width - scaledWidth) / 2.0, y: 0,
width: scaledWidth, height: scaledHeight)
} else {
scaledRect = CGRect(x: 0, y: 0,
width: scaledWidth, height: scaledHeight)
}
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
self.drawInRect(scaledRect)
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return scaledImage
}
}
Useful method for performing drawing in the resulting context
Using calculation of x, y in both branches instead of checking for if widthFactor > heightFactor
import UIKit
extension UIImage {
/**
Creates image of specified size and perfomrs drawing commands
- parameter size: result image size
- parameter drawing: closure that contains deawing operations
- returns: image with the drawing operation
*/
func drawingWithSize(size:CGSize, @noescape drawing:()->()) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
drawing()
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
/**
Scales image to specified size, savin aspect ratio
Crops parts of image that out of provided size
Works as UICntentModeAspectFill
- parameter size: result image size
- returns: scaled image
*/
func scaleAndCrop(toSize size:CGSize) -> UIImage {
// Skip unneeded scaling
guard CGSizeEqualToSize(self.size, size) == false else {
return self
}
let scaleFactor = max(size.width / self.size.width,
size.height / self.size.height)
let (scaledWidth, scaledHeight) = (self.size.width * scaleFactor,
self.size.height * scaleFactor)
let drawingRect = CGRectMake(
(size.width - scaledWidth) / 2.0,
(size.height - scaledHeight) / 2.0,
scaledWidth,
scaledHeight)
let scaledImage = drawingWithSize(size) {
self.drawInRect(drawingRect)
}
return scaledImage
}
}
In my real world code, I use code that passes a context. You can always get context from a valid drawing session and UIKit supports a context stack, so you can push the context, perform drawing, and then pull an image to return and pop the context.
if I understood correctly, the above could also be
let scaleFactor = fitImage ? min(widthFactor, heightFactor) : max(widthFactor, heightFactor)