Search code examples
iosswiftuikituiimage

How to compress image from server to 500kB?


I want to send pictures through the KakaoTalk app with UIActivityViewController. However, the KakaoTalk app has a capacity limit of 500kB per picture. Currently, the photos I receive from the server are over 8MB per picture, so I have to reduce the capacity, but it has not been solved for several days.

Although the image size has been resized and compressed, it is difficult to reduce to a capacity of 500 kB or less no matter how much compression is performed. I want to know another way.

Below is the extension of the UIImage that resized the image.

    func resized(to length: CGFloat) -> UIImage {
        let width = self.size.width
        let height = self.size.height
        let resizeLength: CGFloat = length
        var scale: CGFloat

        if height >= width {
            scale = width <= resizeLength ? 1 : resizeLength / width
        } else {
            scale = height <= resizeLength ? 1 :resizeLength / height
        }
  
        let newHeight = height * scale
        let newWidth = width * scale
        let size = CGSize(width: newWidth, height: newHeight)
        let render = UIGraphicsImageRenderer(size: size)
        let renderImage = render.image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
        return renderImage
    }

I get image from server with this method.

    private func getImageFromURL(imageURL: String, completion: @escaping (UIImage) -> Void) {
        guard let url = URL(string: imageURL) else { return }

        DispatchQueue.global().async { [weak self] in
            if let data = try? Data(contentsOf: url) {
                print("⭐️data from server: \(data.count) bytes")

                if let image = UIImage(data: data) {
                    completion(image)
                }
            }
        }
    }

I share image through UIActivityViewController

    @objc func shareButtonTapped(_ sender: UIButton) {
        memoryViewModel.selectedDataArray = memoryViewModel.selectedImageDictionary.values.compactMap { image in

            // 전송 가능한 사이즈 500KB
            let maxSizeInBytes = 500 * 1024

            // 최초 압축률
            var compressionQuality: CGFloat = 1.0

            // 용량을 줄이기 위한 이미지 resize
            let resizedImage = image.resized(to: 1080)

            // 이미지 -> jpeg (최초 압축률 1.0 적용)
            var imageData = resizedImage.jpegData(compressionQuality: compressionQuality)
            print("🟥 before compression: \(imageData)")

            while imageData?.count ?? 0 > maxSizeInBytes && compressionQuality > 0 {
                compressionQuality -= 0.1
                imageData = image.jpegData(compressionQuality: compressionQuality)
            }
            
            print("🟩 after compression: \(imageData))")
            return image
        }


        let activityViewController = UIActivityViewController(activityItems: memoryViewModel.selectedDataArray, applicationActivities: nil)

        activityViewController.excludedActivityTypes = [UIActivity.ActivityType.addToReadingList]

        present(activityViewController, animated: true, completion: nil)
        
        activityViewController.completionWithItemsHandler = { (activity, completed, items, error) in

            if completed {
                //
            } else {
                //
            }
        }

    }

The following results were obtained through the print statement.

⭐️data from server: 8338918 bytes
🟥 before compression: Optional(11474975 bytes)
🟩 after compression: Optional(633815 bytes))

How can I change the picture I got from the server to the size I want?


Solution

  • The primary issue is with your resized method. Your use of:

    let render = UIGraphicsImageRenderer(size: size)
    

    is relying on a default format and that default gives the image the same scale as the current device. The size you generate is in points. So the actual image, in pixels, is the point size times the image scale. For most devices this will make the size of the image either 4 or 9 times larger.

    Note that your image starts at 8MB and then after you scale it (supposedly to a smaller size), the image blooms to 11MB. Your attempts to then generate a JPEG of lower and lower quality simply doesn't get the image data size small enough.

    To resolve this issue, setup your image render with a scale of 1.0 instead of relying the default scale.

    Update your code as follows:

    var fmt = UIGraphicsImageRendererFormat.default()
    fmt.scale = 1.0
    let render = UIGraphicsImageRenderer(size: size, format: fmt)
    

    This will result in the resized image being the same size in both points and pixels which will give a much smaller initial JPEG. The rest of your logic should now result in the size getting below your required maximum of 500kB before you get to a compression factor of 0.1.

    Here's the complete resized method with the small change in place:

    func resized(to length: CGFloat) -> UIImage {
        let width = self.size.width
        let height = self.size.height
        let resizeLength: CGFloat = length
        var scale: CGFloat
    
        if height >= width {
            scale = width <= resizeLength ? 1 : resizeLength / width
        } else {
            scale = height <= resizeLength ? 1 :resizeLength / height
        }
    
        let newHeight = height * scale
        let newWidth = width * scale
        let size = CGSize(width: newWidth, height: newHeight)
        // Setup the format with a scale of 1.0
        var fmt = UIGraphicsImageRendererFormat.default()
        fmt.scale = 1.0
        let render = UIGraphicsImageRenderer(size: size, format: fmt)
        let renderImage = render.image { _ in
            self.draw(in: CGRect(origin: .zero, size: size))
        }
        return renderImage
    }