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