Skip to content

Instantly share code, notes, and snippets.

@erica
Created May 31, 2016 15:31
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erica/157e20ea0c7e9f28a03a8b12448c8fd0 to your computer and use it in GitHub Desktop.
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)
@Moximillian
Copy link

let fitFillTest = fitImage ? widthFactor < heightFactor : widthFactor > heightFactor
let scaleFactor = fitFillTest ? widthFactor : heightFactor

if I understood correctly, the above could also be

let scaleFactor = fitImage ? min(widthFactor, heightFactor) : max(widthFactor, heightFactor)

@erica
Copy link
Author

erica commented May 31, 2016

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
}

@erica
Copy link
Author

erica commented May 31, 2016

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

@erica
Copy link
Author

erica commented May 31, 2016

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

@romainmenke
Copy link

romainmenke commented May 31, 2016

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

my version

@oleksii-demedetskyi
Copy link

I would recommend to split into several functions (size calculations, image cropping, glue) for unit testing support.

my version

@erica
Copy link
Author

erica commented May 31, 2016

Add asserts for size.width and size.height > 0.0 or some minimum point size as desired. See: http://twitter.com/deadbeefa/status/737722278608142345

@bshirley
Copy link

bshirley commented May 31, 2016

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

@PaulTaykalo
Copy link

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

@erica
Copy link
Author

erica commented May 31, 2016

@PaulTaykalo

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment