iosswiftuiimagensdatauiimagejpegrepresentation

Why doesnt UIImage(data:) create an image at the compressed file size?


I am trying to shrink the size of an image using the code below after converting the UIImage to a compressed JPEG representation and back to a UIImage the UIImage file is still to large how can I shrink the file size of the UIImage?

func changeFileSize()->UIImage{
        var needToCompress:Bool = true
        var compressingValue:CGFloat = 1.0
        let bcf = ByteCountFormatter()
        while needToCompress && compressingValue > 0.0{
            let data =  image.jpegData(compressionQuality: compressingValue)!

            if data.count < 1024 * 100{
                needToCompress = false
                image = UIImage(data: data)
                bcf.allowedUnits = [.useKB] // optional: restricts the units to MB only
                bcf.countStyle = .file
                var  newImage = UIImage(data: data)

               let string = bcf.string(fromByteCount: Int64(newImage!.jpegData(compressionQuality: compressingValue)!.count))
                print("Image Pixels: \(CGSize(width: newImage!.size.width*newImage!.scale, height: newImage!.size.height*newImage!.scale))")
                print("final formatted result to be returned: \(string)")
                print("New comrpession value: \(compressingValue)")
                return UIImage(data:  (newImage?.jpegData(compressionQuality: compressingValue))!)!
                break
            }
            else{
                compressingValue -= 0.1
                bcf.allowedUnits = [.useKB] // optional: restricts the units to MB only
                bcf.countStyle = .file
                let string = bcf.string(fromByteCount: Int64(image.jpegData(compressionQuality: compressingValue)!.count))
                print("formatted result: \(string)")
                print("New comrpession value: \(compressingValue)")
            }
        }

        bcf.allowedUnits = [.useKB] // optional: restricts the units to MB only
        bcf.countStyle = .file
        let string = bcf.string(fromByteCount: Int64(image.jpegData(compressionQuality: 1.0)!.count))


           print("formatted result: \(string)")
            compressionLabel.text = string

            print("Image Pixels: \(CGSize(width: image.size.width*image.scale, height: image.size.height*image.scale))")
return image
        }

Solution

  • The JPEG (or PNG) data just tells UIImage how to re-create the original image. Your code is good at reducing the size of the JPEG data, but that has no effect on the actual image UIKit will render from said JPEG data. Compressing the JPEG data is good for saving locally or sending to an API, but it won't help you save memory during runtime (which I assume is what you want).

    Instead the process you may be looking for is down-sampling. Which will reduce the actual pixels contained in the image, instead of reducing the size of the JPEG representation like you are currently doing.

    You can downsample by making a thumbnail like this:

    func reduceImageSize(for image: UIImage, maxDimension: CGFloat) -> UIImage? {
        if let imageData = image.jpegData(compressionQuality: 1.0),
           let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) {
            
            let downsamplingOptions = [
                kCGImageSourceCreateThumbnailFromImageAlways: true,
                kCGImageSourceThumbnailMaxPixelSize: maxDimension
            ] as CFDictionary
            
            let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsamplingOptions)!
            return UIImage(cgImage: downsampledImage)
        }
        return nil
    }
    

    Here's a quick test to show it working:

    let largeImage = UIImage(named: "fifteenpointsevenmb")!
    print("large image size: \(largeImage.jpegData(compressionQuality: 1.0)!)")
    print("large image dimensions: \(largeImage.size.width) x \(largeImage.size.height)")
        
    let smallImage = reduceImageSize(for: largeImage, maxDimension: 300)!
    print("small image size: \(smallImage.jpegData(compressionQuality: 1.0)!)")
    print("small image dimensions: \(smallImage.size.width) x \(smallImage.size.height)")
    
    >> large image size: 6172365 bytes
    >> large image dimensions: 2532.0 x 1170.0
    >> small image size: 97415 bytes
    >> small image dimensions: 300.0 x 139.0
    

    There is indeed a WWDC 2018 video that discusses much of these details, and shows how to solve the original problem. WWDC18 - Image and Graphics Best Practices. In fact, the code above is a modification of the code provided in the WWDC video. Cheers.