Como comprimir ou reduzir o tamanho de uma imagem antes de enviar para o Parse como PFFile? (Rápido)

87

Eu estava tentando fazer upload de um arquivo de imagem para o Parse depois de tirar uma foto diretamente no telefone. Mas isso lança uma exceção:

Encerrando aplicativo devido à exceção não capturada 'NSInvalidArgumentException', motivo: 'PFFile não pode ser maior que 10485760 bytes'

Aqui está o meu código:

No controlador de primeira visualização:

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

Em vista do controlador que carrega a imagem:

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

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

Mas ainda preciso fazer upload dessa foto para o Parse. Existe alguma maneira de reduzir o tamanho ou a resolução da imagem?

Elvislkm
fonte
Tentei a última proposição de gbk e fount no final que se eu chamar let newData = UIImageJPEGRepresentation (UIImage (data: data), 1) newData.count não é igual a data.count e é realmente maior com um fator de mais de 2. O que é realmente surpreendente para mim! De qualquer forma, obrigado pelo código!
NicoD

Respostas:

185

Sim, você pode usar em UIImageJPEGRepresentationvez de UIImagePNGRepresentationpara reduzir o tamanho do arquivo de imagem. Você pode apenas criar uma extensão UIImage da seguinte maneira:

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

editar / atualizar:

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

Uso:

if let imageData = image.jpeg(.lowest) {
    print(imageData.count)
}
Leo Dabus
fonte
1
Uso do tipo UIImage não declarado. Este é o erro que recebo
Umit Kaya
1
Eu estava retornando imagens com tamanhos de 22 a 25 MB, agora uma fração disso. Muito obrigado! Grande extensão!
Octavio Antonio Cedeño
3
Não há diferença além da sintaxe. Claro que você pode escrever o código manualmente em UIImageJPEGRepresentation(yourImage, 1.0)vez de apenas digitar .jpe deixar o xcode preencher automaticamente o método para você. o mesmo para a enumeração de compressão .whatever.
Leo Dabus
1
@Umitk você deve importar o UIKit
Alan
1
'jpegData (compressionQuality :)' foi renomeado para 'UIImageJPEGRepresentation ( : :)'
5uper_0leh
52

Se você quiser limitar o tamanho da imagem a algum valor concreto, pode fazer o seguinte:

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
    } 
}
gbk
fonte
Esta é uma solução mais abrangente.
Idrees Ashraf
swift 3.1if let data = bits.representation(using: .jpeg, properties: [.compressionFactor:compressingValue])
Jacksonsox
14
Isso é muito caro, você está fazendo uma tarefa tão pesada em um loop while e todas as vezes !! sem condições limitantes.
Amber K
Abrangente, mas muito difícil de memória. Isso trava dispositivos mais antigos com problemas de memória. Tem que haver uma maneira mais barata de fazer isso.
Tofu Warrior
1
Esta é uma ideia realmente ruim, está cheia de casos extremos terríveis. 1) Começa com compressingValue de 1.0, o que significa quase nenhuma compressão. Se as dimensões da imagem forem pequenas, as imagens acabarão tendo muito mais KB do que o necessário. 2) Se as imagens forem grandes, será lento, pois pode recomprimir muitas vezes para ficar abaixo do tamanho desejado. 3) Se as imagens forem muito grandes, ela pode ser comprimida a ponto de parecer lixo. Nesses casos, seria melhor simplesmente deixar de salvar a imagem e dizer ao usuário que ela é muito grande.
Orion Edwards
10
  //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!)!
}
Sr.Javed Multani
fonte
9

Jus Fixing for Xcode 7, testado em 21/09/2015 e funcionando bem:

Basta criar uma extensão da UIImageseguinte forma:

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

Então você pode usá-lo assim:

let imageData = imagePassed.lowestQualityJPEGNSData
Thiago Arreguy
fonte
Agradecimentos essenciais para responder ao proprietário Sr. Thiago e editor Sr. kuldeep
Abhimanyu Rathore
5

Abordagem binária do Swift 4 para compactar a imagem

Acredito que seja muito tarde para responder a essa pergunta, mas aqui está minha solução para a pergunta que é otimizada. Estou usando a pesquisa binária para encontrar o valor ideal. Assim, por exemplo, digamos que a abordagem de subtração normal para atingir 62% exigiria 38 tentativas de compressão, a abordagem * Pesquisa binária ** alcançaria a solução necessária no log máximo (100) = cerca de 7 tentativas.

No entanto, também gostaria de informar que a UIImageJPEGRepresentationfunção não se comporta linearmente, especialmente quando o número chega perto de 1. Aqui está a captura de tela onde podemos ver que a imagem para de compactar depois que o valor flutuante é> 0,995. O comportamento é bastante imprevisível, então é melhor ter um buffer delta que possa lidar com esses casos.

insira a descrição da imagem aqui

Aqui está o código para isso

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)!
    }
}
Rahulg
fonte
Por que você está usando um loop While e incrementando manualmente uma variável? Basta usar for i in 0...13.
Peter Schorn
2

Isso é muito simples com UIImageextensão

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

usar

// max 300kb
image.resizeByByte(maxByte: 300000) { (resizedData) in
    print("image size: \(resizedData.count)")
}
Yusuf
fonte
4
muito lento e sincronizado
iman kazemayni
1
pode acontecer que sua imagem redimensionada nunca seja inferior a 300 kB e você não tenha nenhum substituto para este caso
slxl
pelo menos (compressQuality> = 0) pode ser adicionado ao loop while como uma condição && extra
slxl
2

Atualização do Swift 4.2 . Criei esta extensão para reduzir o tamanho de UIImage.
Aqui você tem dois métodos, o primeiro tira uma porcentagem e o segundo reduz a imagem para 1 MB.
Claro que você pode alterar o segundo método para se tornar 1 KB ou qualquer tamanho que desejar.

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
    }
}
sazz
fonte
2

Em Swift 5 como @Thiago Arreguy responde:

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

}

E você pode ligar assim:

let imageData = imagePassed.lowestQualityJPEGNSData
Eric
fonte
2

In 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!
    }
Sai kumar Reddy
fonte
2

Usando func jpegData(compressionQuality: CGFloat) -> Data? funciona bem se você não precisa compactar para um tamanho específico. No entanto, para certas imagens, acho útil compactar abaixo de um determinado tamanho de arquivo. Nesse caso, jpegDatanão é confiável e a compactação iterativa de uma imagem resulta em um platô no tamanho do arquivo (e pode ser muito caro). Em vez disso, prefiro reduzir o tamanho do próprio UIImage, depois converter para jpegData e verificar se o tamanho reduzido está abaixo do valor que escolhi (dentro de uma margem de erro que defini). Eu ajusto o multiplicador de passo de compressão com base na proporção do tamanho do arquivo atual para o tamanho do arquivo desejado para acelerar as primeiras iterações que são as mais caras.

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

E uso:

let data = image.compress(to: 300)

A resizedextensão UIImage vem de: Como faço para redimensionar a UIImage para reduzir o tamanho da imagem de upload

Iron John Bonney
fonte
1

Swift 3

Resposta @leo-dabus revisada para swift 3

    extension UIImage {
    var uncompressedPNGData: Data?      { return UIImagePNGRepresentation(self)        }
    var highestQualityJPEGNSData: Data? { return UIImageJPEGRepresentation(self, 1.0)  }
    var highQualityJPEGNSData: Data?    { return UIImageJPEGRepresentation(self, 0.75) }
    var mediumQualityJPEGNSData: Data?  { return UIImageJPEGRepresentation(self, 0.5)  }
    var lowQualityJPEGNSData: Data?     { return UIImageJPEGRepresentation(self, 0.25) }
    var lowestQualityJPEGNSData:Data?   { return UIImageJPEGRepresentation(self, 0.0)  }
}
Fozoglu
fonte
1

No Swift 4 criei esta extensão que receberá o tamanho esperado e tentará alcançá-lo.

extension UIImage {

    enum CompressImageErrors: Error {
        case invalidExSize
        case sizeImpossibleToReach
    }
    func compressImage(_ expectedSizeKb: Int, completion : (UIImage,CGFloat) -> Void ) throws {

        let minimalCompressRate :CGFloat = 0.4 // min compressRate to be checked later

        if expectedSizeKb == 0 {
            throw CompressImageErrors.invalidExSize // if the size is equal to zero throws
        }

        let expectedSizeBytes = expectedSizeKb * 1024
        let imageToBeHandled: UIImage = self
        var actualHeight : CGFloat = self.size.height
        var actualWidth : CGFloat = self.size.width
        var maxHeight : CGFloat = 841 //A4 default size I'm thinking about a document
        var maxWidth : CGFloat = 594
        var imgRatio : CGFloat = actualWidth/actualHeight
        let maxRatio : CGFloat = maxWidth/maxHeight
        var compressionQuality : CGFloat = 1
        var imageData:Data = UIImageJPEGRepresentation(imageToBeHandled, compressionQuality)!
        while imageData.count > expectedSizeBytes {

            if (actualHeight > maxHeight || actualWidth > maxWidth){
                if(imgRatio < maxRatio){
                    imgRatio = maxHeight / actualHeight;
                    actualWidth = imgRatio * actualWidth;
                    actualHeight = maxHeight;
                }
                else if(imgRatio > maxRatio){
                    imgRatio = maxWidth / actualWidth;
                    actualHeight = imgRatio * actualHeight;
                    actualWidth = maxWidth;
                }
                else{
                    actualHeight = maxHeight;
                    actualWidth = maxWidth;
                    compressionQuality = 1;
                }
            }
            let rect = CGRect(x: 0.0, y: 0.0, width: actualWidth, height: actualHeight)
            UIGraphicsBeginImageContext(rect.size);
            imageToBeHandled.draw(in: rect)
            let img = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
                if let imgData = UIImageJPEGRepresentation(img!, compressionQuality) {
                if imgData.count > expectedSizeBytes {
                    if compressionQuality > minimalCompressRate {
                        compressionQuality -= 0.1
                    } else {
                        maxHeight = maxHeight * 0.9
                        maxWidth = maxWidth * 0.9
                    }
                }
                imageData = imgData
            }


        }

        completion(UIImage(data: imageData)!, compressionQuality)
    }


}

Usar

        do {
            try UiImageView.image?.compressImage(100, completion: { (image, compressRatio) in
                print(image.size) 
                imageData = UIImageJPEGRepresentation(image, compressRatio)
                base64data = imageData?.base64EncodedString()

            })
        } catch {
                 print("Error")
        }
D Saggin
fonte