93

I've been searching google, and have only come across libraries that either reduce the height/width or some how edit the UIImage appearance via CoreImage. But I have not seen or found one library, post that explains how to reduce image size so when it uploads, it's not the full image size.

so far I have this:

        if image != nil {
        //let data = NSData(data: UIImagePNGRepresentation(image))
        let data = UIImagePNGRepresentation(image)
        body.appendString("--\(boundary)\r\n")
        body.appendString("Content-Disposition: form-data; name=\"image\"; filename=\"randomName\"\r\n")
        body.appendString("Content-Type: image/png\r\n\r\n")
        body.appendData(data)
        body.appendString("\r\n")
    }

and it's sending 12MB photos. How can I reduce this to 1mb? thanks!

Jay
  • 2,591
  • 1
  • 16
  • 28

21 Answers21

214

Xcode 9 • Swift 4 or later

edit/update: For iOS10+ We can use UIGraphicsImageRenderer. For older Swift syntax check edit history.

extension UIImage {
    func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? {
        let canvas = CGSize(width: size.width * percentage, height: size.height * percentage)
        let format = imageRendererFormat
        format.opaque = isOpaque
        return UIGraphicsImageRenderer(size: canvas, format: format).image {
            _ in draw(in: CGRect(origin: .zero, size: canvas))
        }
    }
    func resized(toWidth width: CGFloat, isOpaque: Bool = true) -> UIImage? {
        let canvas = CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))
        let format = imageRendererFormat
        format.opaque = isOpaque
        return UIGraphicsImageRenderer(size: canvas, format: format).image {
            _ in draw(in: CGRect(origin: .zero, size: canvas))
        }
    }
}

Usage:

let image = UIImage(data: try! Data(contentsOf: URL(string:"https://i.stack.imgur.com/Xs4RX.jpg")!))!

let thumb1 = image.resized(withPercentage: 0.1)
let thumb2 = image.resized(toWidth: 72.0)
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • 1
    No biggie but I think it should be `.ScaleAspectFit` with a capital S for the content mode. Anyway, great work Leo, thanks! – Samuel Rosenstein Jul 12 '16 at 17:29
  • 1
    You are welcome. Swift 3 you will need to use lowercase – Leo Dabus Jul 12 '16 at 17:31
  • Value of type 'CALayer' has no member 'render' can i fix this ? – Mr.G Jul 28 '16 at 10:10
  • @Mr.G just look at the edit history. Swift 2 renderInContext – Leo Dabus Jul 28 '16 at 13:54
  • 2
    you should add the swift 2 syntax below. someone copy/pasted your code and gained 9 upvotes.. undeserved if you ask me... – David Seek Nov 29 '16 at 21:26
  • Is there a way to guarantee filesize being 1MB, instead of scaling by percentage or width? – Happiehappie Dec 01 '16 at 06:39
  • I used `.resizeWith(percentage: 0.9)` and got an image with a bigger size :D So now I use percentage not more than 0.8 just to be sure, but overall, good answer – EBDOKUM Dec 05 '16 at 15:24
  • @EBDOKUM This doesn't make any sense. Can you post your code somewhere? myPicture goes from 719x808 to 648x728 here with 0.9 percentage. – Leo Dabus Dec 05 '16 at 15:47
  • @LeoDabus no, I mean the size of the data. I know it's hard to believe, but that's what I've experienced. I'd show you the code but I do not know where to put it, but it's nothing special – EBDOKUM Dec 09 '16 at 17:19
  • 9
    LeoDabus, I think this is the not right answer but very nice solution for other problems. The question here is how can we reduced 12MB to 1MB not based on percentage nor image size (width or height) but based on file size. @EBDOKUM can possibly have that result due to the fact that reducing image size (width & height) doesn't guarantee a file size reduction. Only the size is being scaled down but the file size can increase or decrease depending on the image being used. – nycdanie Dec 19 '16 at 04:07
  • @nycdanie http://stackoverflow.com/questions/29726643/how-to-compress-of-reduce-the-size-of-an-image-before-uploading-to-parse-as-pffi/29726675#29726675. BTW you can't compress a PNG – Leo Dabus Dec 19 '16 at 04:17
  • 3
    @ChrisW. Not talking about photoshop or a web engine. Feel free to post your answer to the question showing how to compress a PNG in iOS (Swift). – Leo Dabus Feb 07 '17 at 16:56
  • 1
    Sorry I'll eat my humble pie on the iOS approach since I'm not an iOS guy by any means and their dev API docs doesn't specifically mention anything for it. So in direct correlation to the iOS portion you got me there sir. – Chris W. Feb 07 '17 at 16:59
  • 1
    howdy @ChrisW., this QA is entirely and totally relating to iOS, and has no connection to anything else :) – Fattie May 30 '17 at 01:09
  • Now use UIGraphicsImageRenderer which always return an image so no more UIImage? – DogCoffee Apr 25 '18 at 05:16
  • 4
    You should add `guard size.width > width else { return self }` so smaller images are not resized to be bigger. – vol Jul 31 '19 at 12:30
72

This is the way which i followed to resize image.

 -(UIImage *)resizeImage:(UIImage *)image
{
   float actualHeight = image.size.height;
   float actualWidth = image.size.width;
   float maxHeight = 300.0;
   float maxWidth = 400.0;
   float imgRatio = actualWidth/actualHeight;
   float maxRatio = maxWidth/maxHeight;
   float compressionQuality = 0.5;//50 percent compression

   if (actualHeight > maxHeight || actualWidth > maxWidth)
   {
    if(imgRatio < maxRatio)
    {
        //adjust width according to maxHeight
        imgRatio = maxHeight / actualHeight;
        actualWidth = imgRatio * actualWidth;
        actualHeight = maxHeight;
    }
    else if(imgRatio > maxRatio)
    {
        //adjust height according to maxWidth
        imgRatio = maxWidth / actualWidth;
        actualHeight = imgRatio * actualHeight;
        actualWidth = maxWidth;
    }
    else
    {
        actualHeight = maxHeight;
        actualWidth = maxWidth;
    }
   }

   CGRect rect = CGRectMake(0.0, 0.0, actualWidth, actualHeight);
   UIGraphicsBeginImageContext(rect.size);
   [image drawInRect:rect];
   UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
   NSData *imageData = UIImageJPEGRepresentation(img, compressionQuality);
   UIGraphicsEndImageContext();

   return [UIImage imageWithData:imageData];

}

Using this method my image having 6.5 MB reduced to 104 KB.

Swift 4 code:

func resize(_ image: UIImage) -> UIImage {
    var actualHeight = Float(image.size.height)
    var actualWidth = Float(image.size.width)
    let maxHeight: Float = 300.0
    let maxWidth: Float = 400.0
    var imgRatio: Float = actualWidth / actualHeight
    let maxRatio: Float = maxWidth / maxHeight
    let compressionQuality: Float = 0.5
    //50 percent compression
    if actualHeight > maxHeight || actualWidth > maxWidth {
        if imgRatio < maxRatio {
            //adjust width according to maxHeight
            imgRatio = maxHeight / actualHeight
            actualWidth = imgRatio * actualWidth
            actualHeight = maxHeight
        }
        else if imgRatio > maxRatio {
            //adjust height according to maxWidth
            imgRatio = maxWidth / actualWidth
            actualHeight = imgRatio * actualHeight
            actualWidth = maxWidth
        }
        else {
            actualHeight = maxHeight
            actualWidth = maxWidth
        }
    }
    let rect = CGRect(x: 0.0, y: 0.0, width: CGFloat(actualWidth), height: CGFloat(actualHeight))
    UIGraphicsBeginImageContext(rect.size)
    image.draw(in: rect)
    let img = UIGraphicsGetImageFromCurrentImageContext()
    let imageData = img?.jpegData(compressionQuality: CGFloat(compressionQuality)) 
    UIGraphicsEndImageContext()
    return UIImage(data: imageData!) ?? UIImage()
}
user4261201
  • 2,324
  • 19
  • 26
  • what about resolution? I would like to make the width a max of 1024, but the height will be dependent on the image, so I can keep the aspect ratio. – Jay Mar 19 '15 at 05:29
  • Here i am compressing the image also. If you want the same resolution comment this line NSData *imageData = UIImageJPEGRepresentation(img, compressionQuality); and we have to select the maxHeight and maxWidth in such a way that the aspect ration should be same. – user4261201 Mar 19 '15 at 05:32
  • 1
    How to give min width and height. So that image in not compressed below particular size – user2185354 Oct 29 '18 at 04:22
  • SWIFT 4.2 change this line: let imageData = UIImageJPEGRepresentation(img!, CGFloat(compressionQuality)) To this: let imageData = img?.jpegData(compressionQuality: CGFloat(compressionQuality)) – ttorbik Feb 26 '19 at 15:13
46

Swift 5 & Xcode 14

I was not satisfied with the solutions here, which generate an image based on a given KB size, since most of them used .jpegData(compressionQuality: x). This method won't work with large images, since even with compression quality set to 0.0, the large image will remain large, e.g. a 10 MB produced by portrait mode of a newer iPhone still will be above 1 MB with compressionQuality set to 0.0.

Therefore I used some answers here and rewrote a Helper Struct which converts an image in a background que:

import UIKit

struct ImageCompressor {
    static func compress(image: UIImage, maxByte: Int,
                         completion: @escaping (UIImage?) -> ()) {
        DispatchQueue.global(qos: .userInitiated).async {
            guard let currentImageSize = image.jpegData(compressionQuality: 1.0)?.count else {
                return completion(nil)
            }
        
            var iterationImage: UIImage? = image
            var iterationImageSize = currentImageSize
            var iterationCompression: CGFloat = 1.0
        
            while iterationImageSize > maxByte && iterationCompression > 0.01 {
                let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
            
                let canvasSize = CGSize(width: image.size.width * iterationCompression,
                                        height: image.size.height * iterationCompression)
                UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
                defer { UIGraphicsEndImageContext() }
                image.draw(in: CGRect(origin: .zero, size: canvasSize))
                iterationImage = UIGraphicsGetImageFromCurrentImageContext()
            
                guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
                    return completion(nil)
                }
                iterationImageSize = newImageSize
                iterationCompression -= percentageDecrease
            }
            completion(iterationImage)
        }
    }

    private static func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
        switch dataCount {
        case 0..<5000000: return 0.03
        case 5000000..<10000000: return 0.1
        default: return 0.2
        }
    }
}

Compress an image to max 2 MB:

        ImageCompressor.compress(image: image, maxByte: 2000000) { image in
            guard let compressedImage = image else { return }
            // Use compressedImage
        }
    }
Ali Pacman
  • 719
  • 7
  • 12
  • 4
    Wish I could upvote this 100 times. The real answer folks. – wxcoder Dec 10 '20 at 23:47
  • 1
    In my opinion, best answer so far. Thumbs up. – Cory Jul 29 '21 at 11:36
  • 2
    this function makes performance better than the others. But I have a problem, there is light border created after using this function. How can I prevent that? Thank you in advanced! – My Will Nov 08 '21 at 04:58
  • @MyWill Are you sure you are using the version from above? I once also had this issue with a black border around the generated image, but I changed the implementation and updated it here and now it's working fine for me. Which iOS version and device are you using? – Ali Pacman Nov 08 '21 at 14:37
  • 1
    @AliPacman I used newest version code, and tested in iPhone X, iPhone 11, iOS 14. There are a light border, not black border around the generated image. You can see here: https://imgur.com/a/EViUjva – My Will Nov 09 '21 at 06:59
  • @MyWill I found out that this issue occurs with the UIImagePickerController, please use the PHPickerViewController instead. – Ali Pacman Dec 06 '21 at 17:11
  • @AliPacman I take a picture directly, not via any image picker. – My Will Dec 07 '21 at 03:54
  • @MyWill Could you please add this below the `compress` function from above and tell me if it solved your problem: https://justpaste.it/7ehw2. If this works I will add it into the answer. I think it is an iOS 14 Bug but I also encountered it in the iPad iOS 15 simulator. – Ali Pacman Dec 08 '21 at 06:33
  • Experienced the same problem. Your fix worked, however, it changed the actual size of the final image (reduced width/height by 10px). – Starwave Apr 14 '22 at 10:40
  • Also the ceiling seems to be off - I added "5000000" to be the max value (5 MB), but resulting image came out as 9.34 MB – Starwave Apr 14 '22 at 10:41
  • 1
    Great solution except for the artefacts issue. – Michel Storms Jan 09 '23 at 23:19
31

In case someone is looking for resizing image to less than 1MB with Swift 3 and 4.

Just copy&paste this extension:

extension UIImage {

func resized(withPercentage percentage: CGFloat) -> UIImage? {
    let canvasSize = CGSize(width: size.width * percentage, height: size.height * percentage)
    UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
    defer { UIGraphicsEndImageContext() }
    draw(in: CGRect(origin: .zero, size: canvasSize))
    return UIGraphicsGetImageFromCurrentImageContext()
}

func resizedTo1MB() -> UIImage? {
    guard let imageData = UIImagePNGRepresentation(self) else { return nil }

    var resizingImage = self
    var imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB

    while imageSizeKB > 1000 { // ! Or use 1024 if you need KB but not kB
        guard let resizedImage = resizingImage.resized(withPercentage: 0.9),
            let imageData = UIImagePNGRepresentation(resizedImage)
            else { return nil }

        resizingImage = resizedImage
        imageSizeKB = Double(imageData.count) / 1000.0 // ! Or devide for 1024 if you need KB but not kB
    }

    return resizingImage
}
}

And use:

let resizedImage = originalImage.resizedTo1MB()

Edit: Please note it's blocking UI, so move to background thread if you think it's the right way for your case.

Tung Fam
  • 7,899
  • 4
  • 56
  • 63
  • In the `while` loop's `guard`, the `UIImagePNGRepresentation` call should use `resizedImage`, not `resizingImage`. Causes it to do an extra loop... – Mike M Oct 30 '17 at 14:04
12

same as Leo Answer but little edits for SWIFT 2.0

 extension UIImage {
    func resizeWithPercentage(percentage: CGFloat) -> UIImage? {
        let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: size.width * percentage, height: size.height * percentage)))
        imageView.contentMode = .ScaleAspectFit
        imageView.image = self
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.renderInContext(context)
        guard let result = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
        UIGraphicsEndImageContext()
        return result
    }

    func resizeWithWidth(width: CGFloat) -> UIImage? {
        let imageView = UIImageView(frame: CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(ceil(width/size.width * size.height)))))
        imageView.contentMode = .ScaleAspectFit
        imageView.image = self
        UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, false, scale)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        imageView.layer.renderInContext(context)
        guard let result = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
        UIGraphicsEndImageContext()
        return result
    }
}
user4261201
  • 2,324
  • 19
  • 26
Jagdeep
  • 1,158
  • 11
  • 16
6

Swift4.2

  let imagedata = yourImage.jpegData(compressionQuality: 0.1)!
Maulik Patel
  • 2,045
  • 17
  • 24
5

Here is user4261201's answer but in swift, that I am currently using:

func compressImage (_ image: UIImage) -> UIImage {

    let actualHeight:CGFloat = image.size.height
    let actualWidth:CGFloat = image.size.width
    let imgRatio:CGFloat = actualWidth/actualHeight
    let maxWidth:CGFloat = 1024.0
    let resizedHeight:CGFloat = maxWidth/imgRatio
    let compressionQuality:CGFloat = 0.5

    let rect:CGRect = CGRect(x: 0, y: 0, width: maxWidth, height: resizedHeight)
    UIGraphicsBeginImageContext(rect.size)
    image.draw(in: rect)
    let img: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
    let imageData:Data = UIImageJPEGRepresentation(img, compressionQuality)!
    UIGraphicsEndImageContext()

    return UIImage(data: imageData)!

}
Jay
  • 2,591
  • 1
  • 16
  • 28
  • 1
    Converting an UIImage to data and back to UIImage is pointless. Btw you are degrading the image quality compressing it. – Leo Dabus Nov 01 '17 at 21:28
  • 1
    @LeoDabus thank you for that insight, I will select your answer as the correct one! – Jay Nov 01 '17 at 21:53
5

I think the core of the question here is how to reliably shrink a UIImage's data to a certain size before uploading to a server, rather than just shrink the UIImage itself.

Using func jpegData(compressionQuality: CGFloat) -> Data? works well if you don't need to compress to a specific size. However, for certain cases, I find it useful to be able to compress below a certain specified file size. In that case, jpegData is unreliable, and iterative compressing of an image this way results in plateauing out on filesize (and can be really expensive). Instead, I prefer to reduce the size of the UIImage itself as in Leo's answer, then convert to jpegData and iteratively check to see if the reduced size is beneath the value I chose (within a margin that I set). I adjust the compression step multiplier based on the ratio of the current filesize to the desired filesize to speed up the first iterations which are the most expensive (since the filesize is the largest at that point).

Swift 5

extension UIImage {
    func resized(withPercentage percentage: CGFloat, isOpaque: Bool = true) -> UIImage? {
        let canvas = CGSize(width: size.width * percentage, height: size.height * percentage)
        let format = imageRendererFormat
        format.opaque = isOpaque
        return UIGraphicsImageRenderer(size: canvas, format: format).image {
            _ in draw(in: CGRect(origin: .zero, size: canvas))
        }
    }
    
    func compress(to kb: Int, allowedMargin: CGFloat = 0.2) -> Data {
        guard kb > 10 else { return Data() } // Prevents user from compressing below a limit (10kb in this case).
        let bytes = kb * 1024
        var compression: CGFloat = 1.0
        let step: CGFloat = 0.05
        var holderImage = self
        var complete = false
        while(!complete) {
            guard let data = holderImage.jpegData(compressionQuality: 1.0) else { break }
            let ratio = data.count / bytes
            if data.count < Int(CGFloat(bytes) * (1 + allowedMargin)) {
                complete = true
                return data
            } else {
                let multiplier:CGFloat = CGFloat((ratio / 5) + 1)
                compression -= (step * multiplier)
            }
            guard let newImage = holderImage.resized(withPercentage: compression) else { break }
            holderImage = newImage
        }
        
        return Data()
    }
}
 

And usage:

let data = image.compress(to: 1000)
Iron John Bonney
  • 2,451
  • 1
  • 17
  • 14
  • 1
    You are returning an empty Data though. I assume you mean `return data`? – Bawenang Rukmoko Pardian Putra Aug 20 '20 at 11:03
  • @BawenangRukmokoPardianPutra That doesn't get called in this function, it's just satisfying the function's return type. The data gets returned in the while loop's if statement. I should probably refactor the answer to throw or use optionals, but went for simplicity of usage. I should also add a guard statement to not allow compressing to unrealistically small values. – Iron John Bonney Aug 20 '20 at 14:30
  • Added guard statement to prevent `compress to 0` case and to limit the compression to a reasonable value (10kb in this example). – Iron John Bonney Aug 20 '20 at 14:43
  • Ah yeah, my bad. I see it now. – Bawenang Rukmoko Pardian Putra Aug 21 '20 at 11:15
  • Can you explain little more this line let multiplier:CGFloat = CGFloat((ratio / 5) + 1) , I didn't understand why you divide / 5 and add + 1 ? @IronJohnBonney – Begbie Nov 10 '21 at 11:35
  • 1
    @Begbie That line is a bit arbitrary. It's original purpose was to adjust the compression percentage based on the ratio of the `current data size : target data size`. The 5 slowed the compression rate somewhat so that you wouldn't compress the data based on the full ratio, which would lead to inaccurate final data size. The 1 ensures that it is always compressing until it reaches the target size, preventing an infinite loop. This line/the magic numbers could be improved. – Iron John Bonney Nov 10 '21 at 13:29
  • I found a problem with if I try to compress high ımage size like 5mb then its not compressing and gives memory leak issue. – Begbie Nov 24 '21 at 09:00
2

If you are uploading image in NSData format, use this :

NSData *imageData = UIImageJPEGRepresentation(yourImage, floatValue);

yourImage is your UIImage. floatvalue is compression value(0.0 to 1.0)

The above is to convert image to JPEG.

For PNGuse : UIImagePNGRepresentation

Note : Above code is in Objective-C. Please check how to define NSData in Swift.

k_g
  • 4,333
  • 2
  • 25
  • 40
Bista
  • 7,869
  • 3
  • 27
  • 55
  • if i'm worried about resolution, how would I set a maximum width, but let the height be determined by the image itself? – Jay Mar 19 '15 at 05:30
  • 2
    UIImagePNGRepresentation has no compression option. – Alec. Nov 25 '16 at 15:10
2

iOS 15+ Swift 5

Part of the solutions here doesn’t answer the question because they are not producing an image that has smaller file size to upload it to backend. It is very important to not uploading big image files to backend when it is not really needed. It will take much more space, will be more expensive to store and take more time to download causing UI to wait for content.

Lots of answers is using either

UIGraphicsImageRenderer(size: canvas).image {
    _ in draw(in: CGRect(origin: .zero, size: canvas))
}

Or older

UIGraphicsGetImageFromCurrentImageContext()

The problem with these solutions is they generate smaller UIImage, but are not changing underlying CGImage so when you try to send image as DATA with .jpegData(compressionQuality:) you will note upload UIImage but data from underlying CGImage which is not resized and has large file size.

UIImage -> CGImage Proxyman POST Image

The other solutions are forcing compression of jpedData to smallest available which produce very large compression and quality loss.

To actually resize image with all underlying stuff and send it as really small best quality jpeg use method preparingThumbnail(of:) and set .jpegData(compressionQuality:) to 8 or 9.

extension UIImage {
    func thumbnail(width: CGFloat) -> UIImage? {
        guard size.width > width else { return self }
        let imageSize = CGSize(
            width: width,
            height: CGFloat(ceil(width/size.width * size.height))
        )
        return preparingThumbnail(of: imageSize)
    }
}

Here is documentation preparingThumbnail(of:)

vandermesis
  • 63
  • 1
  • 8
1

Based on the answer of Tung Fam. To resize to a specific file size. Like 0.7 MB you can use this code.

extension UIImage {

func resize(withPercentage percentage: CGFloat) -> UIImage? {
    var newRect = CGRect(origin: .zero, size: CGSize(width: size.width*percentage, height: size.height*percentage))
    UIGraphicsBeginImageContextWithOptions(newRect.size, true, 1)
    self.draw(in: newRect)
    defer {UIGraphicsEndImageContext()}
    return UIGraphicsGetImageFromCurrentImageContext()
}

func resizeTo(MB: Double) -> UIImage? {
    guard let fileSize = self.pngData()?.count else {return nil}
    let fileSizeInMB = CGFloat(fileSize)/(1024.0*1024.0)//form bytes to MB
    let percentage = 1/fileSizeInMB
    return resize(withPercentage: percentage)
}
}
1

Using this you can control the size that you want:

func jpegImage(image: UIImage, maxSize: Int, minSize: Int, times: Int) -> Data? {
    var maxQuality: CGFloat = 1.0
    var minQuality: CGFloat = 0.0
    var bestData: Data?
    for _ in 1...times {
        let thisQuality = (maxQuality + minQuality) / 2
        guard let data = image.jpegData(compressionQuality: thisQuality) else { return nil }
        let thisSize = data.count
        if thisSize > maxSize {
            maxQuality = thisQuality
        } else {
            minQuality = thisQuality
            bestData = data
            if thisSize > minSize {
                return bestData
            }
        }
    }
    return bestData
}

Method call example:

jpegImage(image: image, maxSize: 500000, minSize: 400000, times: 10)  

It will try to get a file between a maximum and minimum size of maxSize and minSize, but only try times times. If it fails within that time, it will return nil.

Daniel Beltrami
  • 756
  • 9
  • 22
1

I think the easiest way is provided by swift itself to compress the image into compressed data below is the code in swift 4.2

let imageData = yourImageTobeCompressed.jpegData(compressionQuality: 0.5)

and you can send this imageData to upload to server.

Kiran Dube
  • 11
  • 1
  • 2
0

This is what I done in swift 3 for resizing an UIImage. It reduces the image size to less than 100kb. It works proportionally!

extension UIImage {
    class func scaleImageWithDivisor(img: UIImage, divisor: CGFloat) -> UIImage {
        let size = CGSize(width: img.size.width/divisor, height: img.size.height/divisor)
        UIGraphicsBeginImageContext(size)
        img.draw(in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return scaledImage!
    }
}

Usage:

let scaledImage = UIImage.scaleImageWithDivisor(img: capturedImage!, divisor: 3)
ak_ninan
  • 721
  • 7
  • 15
0

Same in Objective-C :

interface :

@interface UIImage (Resize)

- (UIImage *)resizedWithPercentage:(CGFloat)percentage;
- (UIImage *)resizeTo:(CGFloat)weight isPng:(BOOL)isPng jpegCompressionQuality:(CGFloat)compressionQuality;

@end

implementation :

#import "UIImage+Resize.h"

@implementation UIImage (Resize)

- (UIImage *)resizedWithPercentage:(CGFloat)percentage {
    CGSize canvasSize = CGSizeMake(self.size.width * percentage, self.size.height * percentage);
    UIGraphicsBeginImageContextWithOptions(canvasSize, false, self.scale);
    [self drawInRect:CGRectMake(0, 0, canvasSize.width, canvasSize.height)];
    UIImage *sizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return sizedImage;
}

- (UIImage *)resizeTo:(CGFloat)weight isPng:(BOOL)isPng jpegCompressionQuality:(CGFloat)compressionQuality {
    NSData *imageData = isPng ? UIImagePNGRepresentation(self) : UIImageJPEGRepresentation(self, compressionQuality);
    if (imageData && [imageData length] > 0) {
        UIImage *resizingImage = self;
        double imageSizeKB = [imageData length] / weight;

        while (imageSizeKB > weight) {
            UIImage *resizedImage = [resizingImage resizedWithPercentage:0.9];
            imageData = isPng ? UIImagePNGRepresentation(resizedImage) : UIImageJPEGRepresentation(resizedImage, compressionQuality);
            resizingImage = resizedImage;
            imageSizeKB = (double)(imageData.length / weight);
        }

        return resizingImage;
    }
    return nil;
}

Usage :

#import "UIImage+Resize.h"

UIImage *resizedImage = [self.picture resizeTo:2048 isPng:NO jpegCompressionQuality:1.0];
Thomas Mary
  • 1,535
  • 1
  • 13
  • 24
0

When I try to use the accepted answer to resize an image for use in my project it comes out very pixelated and blurry. I ended up with this piece of code to resize images without adding pixelation or blur:

func scale(withPercentage percentage: CGFloat)-> UIImage? {
        let cgSize = CGSize(width: size.width * percentage, height: size.height * percentage)

        let hasAlpha = true
        let scale: CGFloat = 0.0 // Use scale factor of main screen

        UIGraphicsBeginImageContextWithOptions(cgSize, !hasAlpha, scale)
        self.draw(in: CGRect(origin: CGPoint.zero, size: cgSize))

        let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
        return scaledImage
    }
Bernard
  • 1,004
  • 1
  • 12
  • 21
0

I came across this question while investigating image compression and export in Swift, and used it as a starting point to understand the problem better & derive a better technique.

The UIGraphicsBeginImageContext(), UIGraphicsGetImageFromCurrentImageContext(), UIGraphicsEndImageContext() process is an older technique which has been superseded by UIGraphicsImageRenderer, as used by iron_john_bonney and leo-dabus. Their examples were written as extensions on UIImage, whereas I chose to write an independent function. The required differences in approach can be identified by comparison (look at and near the UIGraphicsImageRenderer call), and could easily be ported back into a UIImage extension.

I thought there was potential for improvement on the compression algorithms used here, so I took an approach that started by adjusting the image to have a given total number of pixels, and then compressing it by adjusting the jpeg compression to achieve a specified final file size. The intent of specifying a total number of pixels was to avoid getting tied up in issues with image aspect ratios. Although I haven't done an exhaustive investigation, I suspect scaling an image to a specified total number of pixels will put the final jpeg image file size in a general range, and then jpeg compression can then be used to ensure that a file size limit is achieved with acceptable image quality, providing the initial pixel count isn't too high.

When using UIGraphicsImageRenderer, the CGRect is specified in logical pixels on a host Apple device, which is different to the actual pixels in the output jpeg. Look up device pixel ratios to understand this. To obtain the device pixel ratio, I tried extracting it from the environment, but these techniques caused the playground to crash, so I used a less efficient technique that worked.

If you paste this code into an Xcode playround and place an appropriate .jpg file in the Resources folder, the output file will be placed in the Playground output folder (use Quick Look in the Live View to find this location).

import UIKit

func compressUIImage(_ image: UIImage?, numPixels: Int, fileSizeLimitKB: Double, exportImage: Bool) -> Data {
    var returnData: Data
    if let origWidth = image?.size.width,
       let origHeight = image?.size.height {
        print("Original image size =", origWidth, "*", origHeight, "pixels")

        let imgMult = min(sqrt(CGFloat(numPixels)/(origWidth * origHeight)), 1) // This multiplier scales the image to have the desired number of pixels
        print("imageMultiplier =", imgMult)

        let cgRect = CGRect(origin: .zero, size: CGSize(width: origWidth * imgMult, height: origHeight * imgMult)) // This is in *logical* pixels
        let renderer = UIGraphicsImageRenderer(size: cgRect.size)
        
        let img = renderer.image { ctx in
            image?.draw(in: cgRect)
        }
        
        // Now get the device pixel ratio if needed...
        var img_scale: CGFloat = 1
        if exportImage {
            img_scale = img.scale
        }
        print("Image scaling factor =", img_scale)

        // ...and use to ensure *output* image has desired number of pixels
        let cgRect_scaled = CGRect(origin: .zero, size: CGSize(width: origWidth * imgMult/img_scale, height: origHeight * imgMult/img_scale)) // This is in *logical* pixels
        print("New image size (in logical pixels) =", cgRect_scaled.width, "*", cgRect_scaled.height, "pixels") // Due to device pixel ratios, can have fractional pixel dimensions

        let renderer_scaled = UIGraphicsImageRenderer(size: cgRect_scaled.size)

        let img_scaled = renderer_scaled.image { ctx in
            image?.draw(in: cgRect_scaled)
        }

        var compQual = CGFloat(1.0)

        returnData = img_scaled.jpegData(compressionQuality: 1.0)!
        var imageSizeKB = Double(returnData.count) / 1000.0
        
        print("compressionQuality =", compQual, "=> imageSizeKB =", imageSizeKB, "KB")
        while imageSizeKB > fileSizeLimitKB {
            compQual *= 0.9
            returnData = img_scaled.jpegData(compressionQuality: compQual)!
            imageSizeKB = Double(returnData.count) / 1000.0
            print("compressionQuality =", compQual, "=> imageSizeKB =", imageSizeKB, "KB")
        }
    } else {
        returnData = Data()
    }
    return returnData
}

let image_orig = UIImage(named: "input.jpg")

let image_comp_data = compressUIImage(image_orig, numPixels: Int(4e6), fileSizeLimitKB: 1300, exportImage: true)

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

let filename = getDocumentsDirectory().appendingPathComponent("output.jpg")

try? image_comp_data.write(to: filename)

Sources included Jordan Morgan, and Hacking with Swift.

Andrew Selby
  • 336
  • 2
  • 6
0

In case someone needed, here is an async version modified from Ali Pacman's answer:

import UIKit

extension UIImage {
    func compress(to maxByte: Int) async -> UIImage? {
        let compressTask = Task(priority: .userInitiated) { () -> UIImage? in
            guard let currentImageSize = jpegData(compressionQuality: 1.0)?.count else {
                return nil
            }

            var iterationImage: UIImage? = self
            var iterationImageSize = currentImageSize
            var iterationCompression: CGFloat = 1.0
            
            while iterationImageSize > maxByte && iterationCompression > 0.01 {
                let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
                let canvasSize = CGSize(width: size.width * iterationCompression, height: size.height * iterationCompression)
                
                UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
                defer { UIGraphicsEndImageContext() }
                draw(in: CGRect(origin: .zero, size: canvasSize))
                iterationImage = UIGraphicsGetImageFromCurrentImageContext()
                
                guard let newImageSize = iterationImage?.jpegData(compressionQuality: 1.0)?.count else {
                    return nil
                }
                iterationImageSize = newImageSize
                iterationCompression -= percentageDecrease
            }
            
            return iterationImage
        }
        return await compressTask.value
    }
    
    private func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
        switch dataCount {
        case 0..<3000000: return 0.05
        case 3000000..<10000000: return 0.1
        default: return 0.2
        }
    }
}
David Lee
  • 74
  • 8
  • Compression quality 1.0 will cause additional bits for color to be added to the image. Something like 0.99 will get way better quality and if the original image didn't have these additional bits they won't matter anyways. – förschter Nov 26 '22 at 11:18
0

With Swift 5.5 using async/await and image.pngData() and not .jpegData(compressionQuality: 1.0) to get the correct data representation of the image:


import UIKit

public struct ImageCompressor {
    
    private static func getPercentageToDecreaseTo(forDataCount dataCount: Int) -> CGFloat {
           switch dataCount {
           case 0..<3000000: return 0.05
           case 3000000..<10000000: return 0.1
           default: return 0.2
           }
       }
    static public func compressAsync(image: UIImage, maxByte: Int) async -> UIImage? {
        guard let currentImageSize = image.pngData()?.count else { return nil }
        var iterationImage: UIImage? = image
        var iterationImageSize = currentImageSize
        var iterationCompression: CGFloat = 1.0
        
        while iterationImageSize > maxByte && iterationCompression > 0.01 {
            let percentageDecrease = getPercentageToDecreaseTo(forDataCount: iterationImageSize)
            
            let canvasSize = CGSize(width: image.size.width * iterationCompression,
                                    height: image.size.height * iterationCompression)
            /*
            UIGraphicsBeginImageContextWithOptions(canvasSize, false, image.scale)
            defer { UIGraphicsEndImageContext() }
            image.draw(in: CGRect(origin: .zero, size: canvasSize))
            iterationImage = UIGraphicsGetImageFromCurrentImageContext()
            */
            iterationImage = await image.byPreparingThumbnail(ofSize: canvasSize)
            guard let newImageSize = iterationImage?.pngData()?.count else {
                return nil
            }
            iterationImageSize = newImageSize
            iterationCompression -= percentageDecrease
        }
        
        
        
        return iterationImage
    }
    
}

JeanNicolas
  • 357
  • 3
  • 9
-2

Resize the UIImage using .resizeToMaximumBytes

-2
extension UIImage {
func resized(toValue value: CGFloat) -> UIImage {
    if size.width > size.height {
        return self.resize(toWidth: value)!
    } else {
        return self.resize(toHeight: value)!
    }
}
Mahdi Moqadasi
  • 2,029
  • 4
  • 26
  • 52