Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

commented May 31, 2016

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

This comment has been minimized.

Copy link
Owner Author

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

This comment has been minimized.

Copy link
Owner Author

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

This comment has been minimized.

Copy link
Owner Author

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

This comment has been minimized.

Copy link

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

@AlexeyDemedetskiy

This comment has been minimized.

Copy link

commented May 31, 2016

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

my version

@erica

This comment has been minimized.

Copy link
Owner Author

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented May 31, 2016

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

This comment has been minimized.

Copy link
Owner Author

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
You can’t perform that action at this time.