如何在上传到Parse作为PFFile之前压缩或减小图像的大小?(Swift)

109

我尝试在手机上直接拍照后将图像文件上传到Parse,但是出现了异常:

未捕获的异常 'NSInvalidArgumentException',原因:'PFFile不能大于10485760字节'

这是我的代码:

在第一个视图控制器中:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if (segue.identifier == "getImage")
    {
        var svc = segue.destinationViewController as! ClothesDetail
        svc.imagePassed = imageView.image
    }
}

在上传图像的视图控制器中:

let imageData = UIImagePNGRepresentation(imagePassed)
let imageFile = PFFile(name: "\(picName).png", data: imageData)

var userpic = PFObject(className:"UserPic")
userpic["picImage"] = imageFile`

但我仍然需要将那张照片上传到 Parse。有没有办法减小图片的大小或分辨率?


我尝试了gbk的最后一个建议,发现如果我调用let newData = UIImageJPEGRepresentation(UIImage(data: data), 1),那么newData.count不等于data.count,并且实际上比data.count大得多,超过2倍。这对我来说真的很惊讶!无论如何,感谢提供代码! - NicoD
14个回答

211

你可以使用UIImageJPEGRepresentation代替UIImagePNGRepresentation来减小图像文件的大小。你只需创建如下的UIImage扩展:

Xcode 8.2 • Swift 3.0.2

extension UIImage {
    enum JPEGQuality: CGFloat {
        case lowest  = 0
        case low     = 0.25
        case medium  = 0.5
        case high    = 0.75
        case highest = 1
    }

    /// Returns the data for the specified image in JPEG format.
    /// If the image object’s underlying image data has been purged, calling this function forces that data to be reloaded into memory.
    /// - returns: A data object containing the JPEG data, or nil if there was a problem generating the data. This function may return nil if the image has no data or if the underlying CGImageRef contains data in an unsupported bitmap format.
    func jpeg(_ quality: JPEGQuality) -> Data? {
        return UIImageJPEGRepresentation(self, quality.rawValue)
    }
}

编辑/更新:

Xcode 10 Swift 4.2

extension UIImage {
    enum JPEGQuality: CGFloat {
        case lowest  = 0
        case low     = 0.25
        case medium  = 0.5
        case high    = 0.75
        case highest = 1
    }

    /// Returns the data for the specified image in JPEG format.
    /// If the image object’s underlying image data has been purged, calling this function forces that data to be reloaded into memory.
    /// - returns: A data object containing the JPEG data, or nil if there was a problem generating the data. This function may return nil if the image has no data or if the underlying CGImageRef contains data in an unsupported bitmap format.
    func jpeg(_ jpegQuality: JPEGQuality) -> Data? {
        return jpegData(compressionQuality: jpegQuality.rawValue)
    }
}

使用方法:

if let imageData = image.jpeg(.lowest) {
    print(imageData.count)
}

1
使用未声明的UIImage类型。这是我收到的错误。 - Umit Kaya
1
我之前返回的图像大小为22-25MB,现在只有一小部分大小。非常感谢!这是一个很棒的扩展! - Octavio Antonio Cedeño
3
除了语法以外,没有区别。当然,你可以手动编写代码UIImageJPEGRepresentation(yourImage, 1.0),而不仅仅是输入.jp并让 Xcode 自动完成方法。对于压缩枚举.whatever也是一样的。 - Leo Dabus
1
@Umitk 你应该导入UIKit。 - Alan
1
'jpegData(compressionQuality:)'已更名为'UIImageJPEGRepresentation(::)'。 - Oleh Veheria
显示剩余3条评论

58
如果您想将图像的大小限制在某个具体的值上,可以按照以下步骤进行:
import UIKit

extension UIImage {
    // MARK: - UIImage+Resize
    func compressTo(_ expectedSizeInMb:Int) -> UIImage? {
        let sizeInBytes = expectedSizeInMb * 1024 * 1024
        var needCompress:Bool = true
        var imgData:Data?
        var compressingValue:CGFloat = 1.0
        while (needCompress && compressingValue > 0.0) {
        if let data:Data = UIImageJPEGRepresentation(self, compressingValue) {
            if data.count < sizeInBytes {
                needCompress = false
                imgData = data
            } else {
                compressingValue -= 0.1
            }
        }
    }

    if let data = imgData {
        if (data.count < sizeInBytes) {
            return UIImage(data: data)
        }
    }
        return nil
    } 
}

swift 3.1 if let data = bits.representation(using: .jpeg, properties: [.compressionFactor:compressingValue]) - Jacksonsox
23
这太费钱了,你正在while循环中执行一个耗费大量资源的任务,并且每次都执行!没有任何限制条件... - Amber K
1
全面但是对内存要求很高。这会导致内存问题的旧设备崩溃。必须有更便宜的方法来解决这个问题。 - Tofu Warrior
你忘记了跳出 while 循环 ;) - thibaut noah
3
这是一个非常糟糕的想法,它充满了可怕的边缘情况。1)它从压缩值为1.0开始,这意味着几乎没有压缩。如果图像尺寸很小,则图像将比必要的更多KB。 2)如果图像很大,则会很慢,因为可能需要重新压缩多次才能达到目标大小。3)如果图像非常大,则可能压缩到图像看起来像垃圾的程度。在这些情况下,最好只是保存失败并告诉用户它太大了。 - Orion Edwards
这是一个可怕的解决方案,任何人都不应该使用它。正如其他人已经说过的,尤其是对于大图片而言,这非常昂贵。一定有比仅仅重复压缩数据直到小于或等于某个大小的更好的方法。 - Peter Schorn

15

如果您不需要压缩到特定的大小,使用 func jpegData(compressionQuality: CGFloat) -> Data? 是可以的。但是对于某些图像,我发现能够压缩到特定的文件大小很有用。在这种情况下,jpegData 是不可靠的,并且重复压缩图像会导致文件大小停滞不前(并且可能非常昂贵)。相反,我更喜欢减小 UIImage 的大小,然后将其转换为 jpegData 并检查所选择的减小的大小是否在我设置的误差范围内。根据当前文件大小与所需文件大小的比率调整压缩步骤乘数,以加快最昂贵的第一次迭代。

Swift 5

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()
    }
}

使用方法:

let data = image.compress(to: 300)

UIImage的resized扩展来源于:如何调整UIImage的大小来减小上传图片的大小


不要压缩小于300,否则会破坏图像(缩略图),300表示最大压缩,而上限值(如1000)表示较少的压缩。 @Iron John Bonney感谢您提供的出色解决方案。 - Muhammad Danish Qureshi

11
  //image compression
func resizeImage(image: UIImage) -> UIImage {
    var actualHeight: Float = Float(image.size.height)
    var actualWidth: Float = Float(image.size.width)
    let maxHeight: Float = 300.0
    let maxWidth: Float = 400.0
    var imgRatio: Float = actualWidth / actualHeight
    let maxRatio: Float = maxWidth / maxHeight
    let compressionQuality: Float = 0.5
    //50 percent compression

    if actualHeight > maxHeight || actualWidth > maxWidth {
        if imgRatio < maxRatio {
            //adjust width according to maxHeight
            imgRatio = maxHeight / actualHeight
            actualWidth = imgRatio * actualWidth
            actualHeight = maxHeight
        }
        else if imgRatio > maxRatio {
            //adjust height according to maxWidth
            imgRatio = maxWidth / actualWidth
            actualHeight = imgRatio * actualHeight
            actualWidth = maxWidth
        }
        else {
            actualHeight = maxHeight
            actualWidth = maxWidth
        }
    }

    let rect = CGRectMake(0.0, 0.0, CGFloat(actualWidth), CGFloat(actualHeight))
    UIGraphicsBeginImageContext(rect.size)
    image.drawInRect(rect)
    let img = UIGraphicsGetImageFromCurrentImageContext()
    let imageData = UIImageJPEGRepresentation(img!,CGFloat(compressionQuality))
    UIGraphicsEndImageContext()
    return UIImage(data: imageData!)!
}

10

以下是适用于Xcode 7的修复方法,已于2015年09月21日进行测试并可正常工作:

只需按照以下步骤创建一个UIImage扩展即可:

extension UIImage
{
    var highestQualityJPEGNSData: NSData { return UIImageJPEGRepresentation(self, 1.0)! }
    var highQualityJPEGNSData: NSData    { return UIImageJPEGRepresentation(self, 0.75)!}
    var mediumQualityJPEGNSData: NSData  { return UIImageJPEGRepresentation(self, 0.5)! }
    var lowQualityJPEGNSData: NSData     { return UIImageJPEGRepresentation(self, 0.25)!}
    var lowestQualityJPEGNSData: NSData  { return UIImageJPEGRepresentation(self, 0.0)! }
}

然后你可以像这样使用它:

let imageData = imagePassed.lowestQualityJPEGNSData

感谢回答者Thiago先生和编辑kuldeep先生为此核心问题作出的贡献。 - Abhimanyu Rathore

6

使用Swift 4二进制方法压缩图像

我相信回答这个问题已经有点晚了,但是这里是我使用二进制搜索来寻找最优值的解决方案。例如,通过正常的减法方法达到62%需要38次尝试,而使用**二进制搜索**方法可以在最大log(100) = 大约7次尝试中达到所需的解决方案。

然而,我还想告诉你,UIImageJPEGRepresentation函数的行为不是线性的,特别是当数字接近1时。这是一个截图,在这里我们可以看到,当浮点值 >0.995时,图像停止压缩。行为相当不可预测,因此最好有一个增量缓冲区来处理这种情况。

输入图片说明

以下是代码:

extension UIImage {
    func resizeToApprox(sizeInMB: Double, deltaInMB: Double = 0.2) -> Data {
        let allowedSizeInBytes = Int(sizeInMB * 1024 * 1024)
        let deltaInBytes = Int(deltaInMB * 1024 * 1024)
        let fullResImage = UIImageJPEGRepresentation(self, 1.0)
        if (fullResImage?.count)! < Int(deltaInBytes + allowedSizeInBytes) {
            return fullResImage!
        }

        var i = 0

        var left:CGFloat = 0.0, right: CGFloat = 1.0
        var mid = (left + right) / 2.0
        var newResImage = UIImageJPEGRepresentation(self, mid)

        while (true) {
            i += 1
            if (i > 13) {
                print("Compression ran too many times ") // ideally max should be 7 times as  log(base 2) 100 = 6.6
                break
            }


            print("mid = \(mid)")

            if ((newResImage?.count)! < (allowedSizeInBytes - deltaInBytes)) {
                left = mid
            } else if ((newResImage?.count)! > (allowedSizeInBytes + deltaInBytes)) {
                right = mid
            } else {
                print("loop ran \(i) times")
                return newResImage!
            }
             mid = (left + right) / 2.0
            newResImage = UIImageJPEGRepresentation(self, mid)

        }

        return UIImageJPEGRepresentation(self, 0.5)!
    }
}

你为什么要使用 While 循环并手动递增变量?只需使用 for i in 0...13 - Peter Schorn

5

新的更好的方式

import UIKit

extension UIImage {

    func compress(maxKb: Double) -> Data? {
        let quality: CGFloat = maxKb / self.sizeAsKb()
        let compressedData: Data? = self.jpegData(compressionQuality: quality)
        return compressedData
    }

    func sizeAsKb() -> Double {
        Double(self.pngData()?.count ?? 0 / 1024)
    }
}

// To use
let compressedImage = image.compress(maxKb: 100)

旧的不好的方法

使用UIImage扩展非常简单。

extension UIImage {

func resizeByByte(maxByte: Int, completion: @escaping (Data) -> Void) {
    var compressQuality: CGFloat = 1
    var imageData = Data()
    var imageByte = UIImageJPEGRepresentation(self, 1)?.count
    
    while imageByte! > maxByte {
        imageData = UIImageJPEGRepresentation(self, compressQuality)!
        imageByte = UIImageJPEGRepresentation(self, compressQuality)?.count
        compressQuality -= 0.1
    }
    
    if maxByte > imageByte! {
        completion(imageData)
    } else {
        completion(UIImageJPEGRepresentation(self, 1)!)
    }
}

SWIFT 5

extension UIImage {
    
    func resizeByByte(maxByte: Int, completion: @escaping (Data) -> Void) {
        var compressQuality: CGFloat = 1
        var imageData = Data()
        var imageByte = self.jpegData(compressionQuality: 1)?.count
        
        while imageByte! > maxByte {
            imageData = self.jpegData(compressionQuality: compressQuality)!
            imageByte = self.jpegData(compressionQuality: compressQuality)?.count
            compressQuality -= 0.1
        }
        
        if maxByte > imageByte! {
            completion(imageData)
        } else {
            completion(self.jpegData(compressionQuality: 1)!)
        }
    }
}

使用

// max 300kb
image.resizeByByte(maxByte: 300000) { (resizedData) in
    print("image size: \(resizedData.count)")
}

4
非常慢且同步。 - iman kazemayni
1
你的调整大小后的图像可能永远不会小于300kB,而且你没有任何备选方案来处理这种情况。 - slxl
可以将 (compressQuality >= 0) 添加到 while 循环中作为额外的 && 条件。 - slxl
"新的更好的方法"不起作用。JPEG压缩的最终大小不是线性的,与压缩质量参数也不容易预测。请参见 https://dev59.com/n2cs5IYBdhLWcg3wym4h#12654461 - Joe Tam

5

Swift 4.2 更新。我创建了这个扩展来减小UIImage的大小。
这里有两种方法,第一种以百分比为单位,第二种将图像缩小到1MB。
当然,你可以将第二种方法更改为1KB或任何你想要的大小。

import UIKit

extension UIImage {

    func resized(withPercentage percentage: CGFloat) -> UIImage? {
        let canvasSize = CGSize(width: size.width * percentage, height: size.height * percentage)
        UIGraphicsBeginImageContextWithOptions(canvasSize, false, scale)
        defer { UIGraphicsEndImageContext() }
        draw(in: CGRect(origin: .zero, size: canvasSize))
        return UIGraphicsGetImageFromCurrentImageContext()
    }

    func resizedTo1MB() -> UIImage? {
        guard let imageData = self.pngData() else { return nil }
        let megaByte = 1000.0

        var resizingImage = self
        var imageSizeKB = Double(imageData.count) / megaByte // ! Or devide for 1024 if you need KB but not kB

        while imageSizeKB > megaByte { // ! Or use 1024 if you need KB but not kB
            guard let resizedImage = resizingImage.resized(withPercentage: 0.5),
            let imageData = resizedImage.pngData() else { return nil }

            resizingImage = resizedImage
            imageSizeKB = Double(imageData.count) / megaByte // ! Or devide for 1024 if you need KB but not kB
        }

        return resizingImage
    }
}

2

在Swift中

func ResizeImageFromOriginalSize(targetSize: CGSize) -> UIImage {
        var actualHeight: Float = Float(self.size.height)
        var actualWidth: Float = Float(self.size.width)
        let maxHeight: Float = Float(targetSize.height)
        let maxWidth: Float = Float(targetSize.width)
        var imgRatio: Float = actualWidth / actualHeight
        let maxRatio: Float = maxWidth / maxHeight
        var compressionQuality: Float = 0.5
        //50 percent compression

        if actualHeight > maxHeight || actualWidth > maxWidth {
            if imgRatio < maxRatio {
                //adjust width according to maxHeight
                imgRatio = maxHeight / actualHeight
                actualWidth = imgRatio * actualWidth
                actualHeight = maxHeight
            }
            else if imgRatio > maxRatio {
                //adjust height according to maxWidth
                imgRatio = maxWidth / actualWidth
                actualHeight = imgRatio * actualHeight
                actualWidth = maxWidth
            }
            else {
                actualHeight = maxHeight
                actualWidth = maxWidth
                compressionQuality = 1.0
            }
        }
        let rect = CGRect(x: 0.0, y: 0.0, width: CGFloat(actualWidth), height: CGFloat(actualHeight))
        UIGraphicsBeginImageContextWithOptions(rect.size, false, CGFloat(compressionQuality))
        self.draw(in: rect)
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage!
    }

2
在Swift 5中,就像@Thiago Arreguy的回答一样:
extension UIImage {

    var highestQualityJPEGNSData: Data { return self.jpegData(compressionQuality: 1.0)! }
    var highQualityJPEGNSData: Data    { return self.jpegData(compressionQuality: 0.75)!}
    var mediumQualityJPEGNSData: Data  { return self.jpegData(compressionQuality: 0.5)! }
    var lowQualityJPEGNSData: Data     { return self.jpegData(compressionQuality: 0.25)!}
    var lowestQualityJPEGNSData: Data  { return self.jpegData(compressionQuality: 0.0)! }

}

你可以这样调用:

let imageData = imagePassed.lowestQualityJPEGNSData

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接