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.