3
\$\begingroup\$

getFinalImageData(:UIImage) takes a UIImage and downscales its size dimensions (to 400 points, in this example) and returns it as Data that has been compressed to be within the byte limit (using the two UIImage extensions). This function works great but I would love to get some other eyes on it.

typealias Bytes = Int64
extension Bytes {
 static let KB300: Int64 = 300_000
 static let MB1: Int64 = 1_048_576
 static let MB2: Int64 = 2_097_152
 static let MB80: Int64 = 83_886_080
 static let MB100: Int64 = 104_857_600
 static let MB120: Int64 = 125_829_120
}
extension UIImage {
 func compressedToJPEGData(maxBytes limit: Int64) -> Data? {
 /// These are compression multipliers (1 = least compression,
 /// 0 = most compression) to define the granularity of
 /// compression reduction.
 let multipliers: [CGFloat] = [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0]
 
 for m in multipliers {
 if let data = jpegData(compressionQuality: m),
 data.count <= limit {
 return data
 }
 }
 return nil
 }
 
 func downscaled(maxPoints limit: CGFloat) -> UIImage {
 let width = size.width
 let height = size.height
 let maxLength = max(width, height)
 
 guard maxLength > limit else {
 return self
 }
 let downscaleDivisor = maxLength / limit
 let downscaledDimensions = CGSize(width: width / downscaleDivisor,
 height: height / downscaleDivisor)
 
 return UIGraphicsImageRenderer(size: downscaledDimensions).image { (_) in
 draw(in: CGRect(origin: .zero, size: downscaledDimensions))
 }
 }
}
func getFinalImageData(from image: UIImage) -> Data? {
 let downscaled = image.downscaled(maxPoints: 400)
 if let compressed = downscaled.compressedToJPEGData(maxBytes: Bytes.KB300) {
 return compressed
 }
 return nil
}
asked Jun 11, 2022 at 0:18
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

A couple of quick observations:

  1. In UIGraphicsImageRenderer, make sure to set the scale in the UIGraphicsImageRendererFormat to be the scale of the image. Usually it is 1.0, unless it is a screen snapshot, in which case it is the scale of the device that it was snapshotted from. Bottom line, UIGraphicsImageRenderer will default to the scale of the device, which may make your image much bigger than you intended. E.g., take a ×ばつ400 px image, and the renderer on a ×ばつ device will make it ×ばつ1200 with no additional data, which is the exact opposite of what you obviously intend. The correct scale is a function of the image in question, not the device.

  2. Do you have access to the original Data associated with this asset? (Note, I am not asking about the output of pngData or jpegData, but the raw data of the original asset.) E.g., a photo taken with a camera generally has decent JPEG compression with it already, and round-tripping it through UIImage and then adding JPEG compression like 0.9 can actually simultaneously lose data and make it bigger. Bottom line, make the decision to downscale/compress only if the original raw asset demands it.

  3. Once you decide that the original asset is really too big, on the array of JPEG compression rates, you should probably remove the 1.0 scale from the list, as that will make the asset huge with absolutely no image improvement. I think 0.8 is a fine starting point. 0.9 if you want to be conservative. Try it out and you will see what I mean.

    Bottom line 1.0 compression makes it much bigger. 0.7-0.8 results in barely visible JPEG artifacts, and it falls apart quickly below 0.6, IMHO.


Below you mention that you are using UIImagePickerController. If so, consider the following:


let formatter: NumberFormatter = {
 let formatter = NumberFormatter()
 formatter.numberStyle = .decimal
 return formatter
}()
extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
 func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
 picker.dismiss(animated: true)
 guard let asset = info[.phAsset] as? PHAsset else { return }
 asset.requestContentEditingInput(with: nil) { [self] input, info in
 guard
 let fileURL = input?.fullSizeImageURL,
 let data = try? Data(contentsOf: fileURL),
 let image = UIImage(data: data),
 let data1 = image.jpegData(compressionQuality: 1),
 let data9 = image.jpegData(compressionQuality: 0.9),
 let data8 = image.jpegData(compressionQuality: 0.8),
 let data7 = image.jpegData(compressionQuality: 0.7),
 let data6 = image.jpegData(compressionQuality: 0.6)
 else { return }
 print("original", formatter.string(for: data.count)!) // 2,227,880
 print(1.0, formatter.string(for: data1.count)!) // 6,242,371
 print(0.9, formatter.string(for: data9.count)!) // 3,672,570
 print(0.8, formatter.string(for: data8.count)!) // 3,004,577
 print(0.7, formatter.string(for: data7.count)!) // 2,576,892
 print(0.6, formatter.string(for: data6.count)!) // 1,958,503
 }
 }
}

I am obviously not suggesting the above implementation in your code, but rather merely illustrating that (a) one way you can fetch the original asset; and (b) to show how its size compares to jpegData of various compression quality settings after round-tripping to a UIImage. Obviously, my numbers are from a random image in my photo library, and your values will vary, but the above sizes are entirely consistent with my historical experiments with various compression settings for JPEGs.

Perhaps needless to say, if accessing the photos library, you must request permission:

PHPhotoLibrary.requestAuthorization { granted in
 print(granted)
}

And set NSPhotoLibraryUsageDescription in the Info.plist.

answered Aug 20, 2022 at 2:06
\$\endgroup\$
2
  • \$\begingroup\$ Regarding point 1, I think that's sound advice and I took it. Regarding point 2, in its current usage, the image will always be imported from an iPhone Photo Library using UIImagePickerController; there is no support for direct-from-camera or importing images otherwise. What do you suggest here? Regarding point 3, I was under the assumption that a scale of 1 was equal to doing nothing. And so if the image is already within the byte limit, do nothing. Is this not the case? \$\endgroup\$ Commented Aug 21, 2022 at 1:37
  • \$\begingroup\$ Re 2, when using UIImagePickerController, you can fetch the original asset URL. See updated answer for one example. Re 3, in my code snippet I take 2.2 mb asset, make UIImage and then look at the resulting jpegData with compression values of 1.0, 0.9, 0.8, 0.7, and 0.6, and you can see that it didn't end up being smaller than the original asset until I got below 0.7. Your mileage may vary. \$\endgroup\$ Commented Aug 21, 2022 at 20:22

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.