Search code examples
swiftuikituiimagepngcore-graphics

How to reduce file size of png image in Swift by reducing resolution


I'm looking for ways to reduce the file size of a PNG file via an image resize, not further compression.

There is a lot of sample code here that compresses a UIImage by turning it into a JPEG.

Like this:

How do I resize the UIImage to reduce upload image size

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

This code allows you to pick a dimension to resize the image but it doesn't let you control the file size.

Here is an example of resizing down to a specific size but because it uses JPEG data it loses the transparency: How to compress of reduce the size of an image before uploading to Parse as PFFile? (Swift)

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 {
        let bytes = kb * 1024
        var compression: CGFloat = 1.0
        let step: CGFloat = 0.05
        var holderImage = self
        var complete = false
        while(!complete) {
            if let data = holderImage.jpegData(compressionQuality: 1.0) {
                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()
    }
}

What I have is a PNG image where I have to preserve transparency while keeping the file size below 500k (hard limit - because the server limits it).

How can I do that in Swift?


Solution

  • This is a modified version of the code you posted in your question. This implementation gets the PNG data of the image instead of the JPEG data. This code also makes use of the UIImage preparingThumbnail(of:) method to produce the smaller image. This keeps any existing transparency in place. I've also changed the code to always use the original image for each iteration to ensure a better quality image and made some other changes while keeping the original algorithm in place.

    extension UIImage {
        func resized(withPercentage percentage: CGFloat) -> UIImage? {
            let newSize = CGSize(width: size.width * percentage, height: size.height * percentage)
    
            return self.preparingThumbnail(of: newSize)
        }
    
        func compress(to kb: Int, allowedMargin: CGFloat = 0.2) -> Data? {
            let bytes = kb * 1024
            let threshold = Int(CGFloat(bytes) * (1 + allowedMargin))
            var compression: CGFloat = 1.0
            let step: CGFloat = 0.05
            var holderImage = self
            while let data = holderImage.pngData() {
                let ratio = data.count / bytes
                if data.count < threshold {
                    return data
                } else {
                    let multiplier = CGFloat((ratio / 5) + 1)
                    compression -= (step * multiplier)
    
                    guard let newImage = self.resized(withPercentage: compression) else { break }
                    holderImage = newImage
                }
            }
    
            return nil
        }
    }
    

    You can test the extension in a Playground with code such as the following:

    // Change the image name as needed
    if let url = Bundle.main.url(forResource: "ImageWithAlpha", withExtension: "png") {
        if let image = UIImage(contentsOfFile: url.path) {
            if let data = image.compress(to: 500) {
                print(data.count) // Shows the final byte count
                let img = UIImage(data: data)!
                print(img) // Shows the final resolution
            } else {
                print("Couldn't resize the image")
            }
        } else {
            print("Not a valid image")
        }
    } else {
        print("No such resource")
    }
    

    Be sure to add a PNG image to the Playground resources folder.