Skip to content

Instantly share code, notes, and snippets.

@tomasbasham
Last active February 1, 2024 19:04
Show Gist options
  • Star 89 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save tomasbasham/10533743 to your computer and use it in GitHub Desktop.
Save tomasbasham/10533743 to your computer and use it in GitHub Desktop.
Scale a UIImage to any given rect keeping the aspect ratio
@implementation UIImage (scale)
/**
* Scales an image to fit within a bounds with a size governed by
* the passed size. Also keeps the aspect ratio.
*
* Switch MIN to MAX for aspect fill instead of fit.
*
* @param newSize the size of the bounds the image must fit within.
* @return a new scaled image.
*/
- (UIImage *)scaleImageToSize:(CGSize)newSize {
CGRect scaledImageRect = CGRectZero;
CGFloat aspectWidth = newSize.width / self.size.width;
CGFloat aspectHeight = newSize.height / self.size.height;
CGFloat aspectRatio = MIN ( aspectWidth, aspectHeight );
scaledImageRect.size.width = self.size.width * aspectRatio;
scaledImageRect.size.height = self.size.height * aspectRatio;
scaledImageRect.origin.x = (newSize.width - scaledImageRect.size.width) / 2.0f;
scaledImageRect.origin.y = (newSize.height - scaledImageRect.size.height) / 2.0f;
UIGraphicsBeginImageContextWithOptions( newSize, NO, 0 );
[self drawInRect:scaledImageRect];
UIImage* scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
@end
@Bodacious
Copy link

In Rubymotion

class UIImage

  # Scales an image to fit within a bounds with a size governed by
  # the passed size. Also keeps the aspect ratio.
  # 
  # newSize - the CGSize of the bounds the image must fit within.
  # aspect  - A Symbol stating the aspect mode (defaults: :min)
  #
  # Returns a new scaled UIImage
  def scaleImageToSize(newSize, aspect = :fit)
    scaledImageRect = CGRectZero

    aspectRules  = { :fill => :max } # else :min

    aspectWidth  = Rational(newSize.width,  size.width)
    aspectHeight = Rational(newSize.height, size.height)

    aspectRatio  = [aspectWidth, aspectHeight].send(aspectRules[aspect] || :min)

    scaledImageRect.size        = (size.width * aspectRatio).round
    scaledImageRect.size.height = (size.height * aspectRatio).round

    scaledImageRect.origin.x = Rational(newSize.width - scaledImageRect.size.width, 2.0).round
    scaledImageRect.origin.y = Rational(newSize.height - scaledImageRect.size.height, 2.0).round

    UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
    drawInRect(scaledImageRect)
    scaledImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    scaledImage
  end

end

@reinder42
Copy link

In Swift 2 (with keeping aspect ratio)

func imageWithSize(size:CGSize) -> UIImage
{
    var scaledImageRect = CGRect.zero;

    let aspectWidth:CGFloat = size.width / self.size.width;
    let aspectHeight:CGFloat = size.height / self.size.height;
    let aspectRatio:CGFloat = min(aspectWidth, aspectHeight);

    scaledImageRect.size.width = self.size.width * aspectRatio;
    scaledImageRect.size.height = self.size.height * aspectRatio;
    scaledImageRect.origin.x = (size.width - scaledImageRect.size.width) / 2.0;
    scaledImageRect.origin.y = (size.height - scaledImageRect.size.height) / 2.0;

    UIGraphicsBeginImageContextWithOptions(size, false, 0);

    self.drawInRect(scaledImageRect);

    let scaledImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return scaledImage;
}

If you want "aspect fill", instead of "aspect fit", change function min to max.

@noveleven
Copy link

Good job!

@nelsongc
Copy link

great job reinderdevries

@nabbestemmia
Copy link

Thank you reinderdevries!!!

@akshaynhegde
Copy link

Thanks for the code everyone! Here is a Swift 3 version.

// MARK: - Image Scaling.
extension UIImage {


    /// Scales an image to fit within a bounds with a size governed by the passed size. Also keeps the aspect ratio.
    /// Switch MIN to MAX for aspect fill instead of fit.
    ///
    /// - parameter newSize: newSize the size of the bounds the image must fit within.
    ///
    /// - returns: a new scaled image.
    func scaleImageToSize(newSize: CGSize) -> UIImage {
        var scaledImageRect = CGRect.zero

        let aspectWidth = newSize.width/size.width
        let aspectheight = newSize.height/size.height

        let aspectRatio = max(aspectWidth, aspectheight)

        scaledImageRect.size.width = size.width * aspectRatio;
        scaledImageRect.size.height = size.height * aspectRatio;
        scaledImageRect.origin.x = (newSize.width - scaledImageRect.size.width) / 2.0;
        scaledImageRect.origin.y = (newSize.height - scaledImageRect.size.height) / 2.0;

        UIGraphicsBeginImageContext(newSize)
        draw(in: scaledImageRect)
        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return scaledImage!
    }
}

@jamesfzhang
Copy link

jamesfzhang commented Jan 17, 2017

This method returns a blurry version of the original image for me even though it's scaled down.

@Oyvindkg
Copy link

Oyvindkg commented Feb 4, 2017

Modification of @akshaynhegde for a more generic implementation using an enum for scaling mode, and a pretty interface:

The final interface is as simple as this:

image.scaled(to: size)
image.scaled(to: size, scalingMode: .aspectFill)
image.scaled(to: size, scalingMode: .aspectFit)

.. and is implemented like this:

// MARK: - Image Scaling.
extension UIImage {
    
    /// Represents a scaling mode
    enum ScalingMode {
        case aspectFill
        case aspectFit

        /// Calculates the aspect ratio between two sizes
        /// 
        /// - parameters:
        ///     - size:      the first size used to calculate the ratio
        ///     - otherSize: the second size used to calculate the ratio
        /// 
        /// - return: the aspect ratio between the two sizes
        func aspectRatio(between size: CGSize, and otherSize: CGSize) -> CGFloat {
            let aspectWidth  = size.width/otherSize.width
            let aspectHeight = size.height/otherSize.height

            switch self {
            case .aspectFill:
               return max(aspectWidth, aspectHeight)
            case .aspectFit:
                return min(aspectWidth, aspectHeight)
            }
        }
    }

    /// Scales an image to fit within a bounds with a size governed by the passed size. Also keeps the aspect ratio.
    ///
    /// - parameter:
    ///     - newSize:     the size of the bounds the image must fit within.
    ///     - scalingMode: the desired scaling mode
    ///
    /// - returns: a new scaled image.
    func scaled(to newSize: CGSize, scalingMode: UIImage.ScalingMode = .aspectFill) -> UIImage {
        
        let aspectRatio = scalingMode.aspectRatio(between: newSize, and: size)
        
        /* Build the rectangle representing the area to be drawn */
        var scaledImageRect = CGRect.zero

        scaledImageRect.size.width  = size.width * aspectRatio
        scaledImageRect.size.height = size.height * aspectRatio
        scaledImageRect.origin.x    = (newSize.width - size.width * aspectRatio) / 2.0
        scaledImageRect.origin.y    = (newSize.height - size.height * aspectRatio) / 2.0

        /* Draw and retrieve the scaled image */
        UIGraphicsBeginImageContext(newSize)

        draw(in: scaledImageRect)
        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()

        UIGraphicsEndImageContext()

        return scaledImage!
    }
}

This could be improved to support more scaling modes such as .fit, but the topic says keeping the aspect ratio.

@jessejiang0214
Copy link

In Xamarin.iOS

public static class UIImageHelper
{
    public static UIImage ScaledImage(this UIImage self, CGSize newSize)
    {
        CGRect scaledImageRect = CGRect.Empty;

        double aspectWidth = newSize.Width / self.Size.Width;
        double aspectHeight = newSize.Height / self.Size.Height;
        double aspectRatio = Math.Min(aspectWidth, aspectHeight);

        scaledImageRect.Size = new CGSize(self.Size.Width * aspectRatio, self.Size.Height * aspectRatio);
        scaledImageRect.X = (newSize.Width - scaledImageRect.Size.Width) / 2.0f;
        scaledImageRect.Y = (newSize.Height - scaledImageRect.Size.Height) / 2.0f;

        UIGraphics.BeginImageContextWithOptions(newSize, false, 0);
        self.Draw(scaledImageRect);

        UIImage scaledImage = UIGraphics.GetImageFromCurrentImageContext();
        UIGraphics.EndImageContext();
        return scaledImage;
    }
}

@Tantalum73
Copy link

@Oyvindkg, if you call this method with .aspectFit it produces an image with the specified 'newSize' size (let's say 800x800). What you would expect to get is an image scaled according to its aspect ratio with max 800 width or height (depending on the image size and form factor). In this example, the result is a squared, resized image of (e.g.) a landscape picture.

This can be fixed by setting the origin coordinates to 0:

scaledImageRect.origin.x = 0
scaledImageRect.origin.y = 0

and by using the computed, scaled rect as input for the UIGraphicsContext to begin:

UIGraphicsBeginImageContext(scaledImageRect.size)

@cyberespia
Copy link

cyberespia commented Mar 25, 2018

And if the image is in a URL type http://domain.com/myimage.jpg obtained with JSON, as I do to resize it to show it in a UIImage so that it keeps the ratio?

@lee-burlak
Copy link

lee-burlak commented Jun 28, 2018

Thank you for your work, guys!

@akshaynhegde, @Oyvindkg, the method returns a blurry version of the original image, it is scaled, but blurry. Any idea how to fix it?

screen shot 2018-06-28 at 22 11 41

@shinrikiken
Copy link

Swift 4

extension UIImage {
  func scale(with size: CGSize) -> UIImage? {
    var scaledImageRect = CGRect.zero
    
    let aspectWidth:CGFloat = size.width / self.size.width
    let aspectHeight:CGFloat = size.height / self.size.height
    let aspectRatio:CGFloat = min(aspectWidth, aspectHeight)
    
    scaledImageRect.size.width = self.size.width * aspectRatio
    scaledImageRect.size.height = self.size.height * aspectRatio
    scaledImageRect.origin.x = (size.width - scaledImageRect.size.width) / 2.0
    scaledImageRect.origin.y = (size.height - scaledImageRect.size.height) / 2.0
    
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    
    self.draw(in: scaledImageRect)
    
    let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return scaledImage
  }
}

@hemangshah
Copy link

@shinrikiken - your code is not giving me the perfect image which should fits to the image size.

@hemangshah
Copy link

hemangshah commented Apr 15, 2019

@lee-ho, perhaps you can replace this line

UIGraphicsBeginImageContext(newSize)

with this

UIGraphicsBeginImageContextWithOptions(newSize, false, UIScreen.main.scale)

and try?

@fenixkim
Copy link

Does anyone have the version of this to work in NSImage (for MacOS)?

@fenixkim
Copy link

Swift 5 (Mac Os)

This working for me but I'm sure if is the best approach

extension NSImage {
    
    /// Represents a scaling mode
    enum ScalingMode {
        case aspectFill
        case aspectFit
        
        /// Calculates the aspect ratio between two sizes
        ///
        /// - parameters:
        ///     - size:      the first size used to calculate the ratio
        ///     - otherSize: the second size used to calculate the ratio
        ///
        /// - return: the aspect ratio between the two sizes
        func aspectRatio(between size: CGSize, and otherSize: CGSize) -> CGFloat {
            let aspectWidth  = size.width/otherSize.width
            let aspectHeight = size.height/otherSize.height
            
            switch self {
            case .aspectFill:
                return max(aspectWidth, aspectHeight)
            case .aspectFit:
                return min(aspectWidth, aspectHeight)
            }
        }
    }
    
    /// Scales an image to fit within a bounds with a size governed by the passed size. Also keeps the aspect ratio.
    ///
    /// - parameter:
    ///     - newSize:     the size of the bounds the image must fit within.
    ///     - scalingMode: the desired scaling mode
    ///
    /// - returns: a new scaled image.
    func scaled(to newSize: CGSize, scalingMode: ScalingMode = .aspectFill) -> NSImage {
        
        let aspectRatio = scalingMode.aspectRatio(between: newSize, and: size)
        
        /* Build the rectangle representing the area to be drawn */
        var scaledImageRect = CGRect.zero
        
        scaledImageRect.size.width  = size.width * aspectRatio
        scaledImageRect.size.height = size.height * aspectRatio
        scaledImageRect.origin.x    = (newSize.width - size.width * aspectRatio) / 2.0
        scaledImageRect.origin.y    = (newSize.height - size.height * aspectRatio) / 2.0
        
        let scaledImage = NSImage(size: newSize)
        scaledImage.lockFocus()
        draw(in: scaledImageRect)
        scaledImage.unlockFocus()
        
        return scaledImage
    }
}

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