0

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?

HangarRash
  • 7,314
  • 5
  • 5
  • 32
erotsppa
  • 14,248
  • 33
  • 123
  • 181

2 Answers2

1

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.

HangarRash
  • 7,314
  • 5
  • 5
  • 32
-1

you can downsampling your image like that

extension UIImage {
    var asSmallImage: UIImage? {
        let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
        
        guard let cgImage = self.cgImage else { return nil }
        
        guard let imgData = cgImage.isPNG ? self.pngData() : self.jpegData(compressionQuality: 0.75) else { return nil }
        
        guard let source = CGImageSourceCreateWithData(imgData as CFData, sourceOptions) else { return nil }
        
        let downsampleOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: 1_000,
        ] as CFDictionary

        guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions) else { return nil }

        let data = NSMutableData()
        guard let imageDestination = CGImageDestinationCreateWithData(data, kUTTypeJPEG, 1, nil) else { return nil }

        // Don't compress PNGs, they're too pretty
        let destinationProperties = [kCGImageDestinationLossyCompressionQuality: cgImage.isPNG ? 1.0 : 0.75] as CFDictionary
        CGImageDestinationAddImage(imageDestination, cgImage, destinationProperties)
        CGImageDestinationFinalize(imageDestination)

        let image = UIImage(data: data as Data)
        return image
    }
}

here you can modify kCGImageSourceThumbnailMaxPixelSize property to resize your image.

Credit : I followed it

mubin
  • 1
  • 2
  • Yes that works but in order to hit the 500k size limit I guess this would have to be looped and checked each iteration? – erotsppa Mar 27 '23 at 14:51
  • Most of this code is copied from https://stackoverflow.com/a/69402031/20287183. Please give credit where due. And your copy forgot the code for the `isPNG` property. – HangarRash Mar 27 '23 at 16:22