Come comprimere o ridurre le dimensioni di un'immagine prima di caricarla su Parse as PFFile? (Swift)


88

Stavo cercando di caricare un file immagine su Parse dopo aver scattato la foto direttamente sul telefono. Ma genera un'eccezione:

Chiusura dell'app a causa di un'eccezione non rilevata "NSInvalidArgumentException", motivo: "PFFile non può essere maggiore di 10485760 byte"

Ecco il mio codice:

In first view controller:

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

In view controller che carica l'immagine:

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

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

Ma devo ancora caricare quella foto su Parse. C'è un modo per ridurre le dimensioni o la risoluzione dell'immagine?


Ho provato l'ultima proposizione di gbk e fount alla fine che se chiamo let newData = UIImageJPEGRepresentation (UIImage (data: data), 1) newData.count non è uguale a data.count ed è davvero più grande con un factore di più di 2. Il che per me è davvero sorprendente! Comunque, grazie per il codice!
NicoD

Risposte:


188

Sì, puoi usare UIImageJPEGRepresentationinvece di UIImagePNGRepresentationper ridurre le dimensioni del file immagine. Puoi semplicemente creare un'estensione UIImage come segue:

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

modifica / aggiorna:

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

Utilizzo:

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

1
Uso del tipo non dichiarato UIImage. Questo è l'errore che ricevo
Umit Kaya

1
Restituivo immagini con dimensioni di 22-25 MB, ora una frazione di quella. Grazie mille! Grande estensione!
Octavio Antonio Cedeño

3
Non c'è differenza oltre alla sintassi. Ovviamente puoi scrivere manualmente il codice UIImageJPEGRepresentation(yourImage, 1.0)invece di digitare .jpe lasciare che xcode completi automaticamente il metodo per te. lo stesso per l'enumerazione della compressione .whatever.
Leo Dabus

1
@Umitk dovresti importare UIKit
Alan

1
"jpegData (compressionQuality :)" è stato rinominato in "UIImageJPEGRepresentation ( : :)"
5uper_0leh

53

Se vuoi limitare la dimensione dell'immagine a un valore concreto puoi fare come segue:

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

Questa è una soluzione più completa.
Idrees Ashraf

swift 3.1if let data = bits.representation(using: .jpeg, properties: [.compressionFactor:compressingValue])
Jacksonsox

16
Questo è molto costoso, stai facendo un compito così pesante su un ciclo while e ogni volta !! nessuna condizione limite ..
Amber K

Completo ma davvero duro per la memoria. Ciò provoca l'arresto anomalo dei dispositivi meno recenti con problemi di memoria. Ci deve essere un modo più economico per farlo.
Tofu Warrior

1
Questa è una pessima idea, è piena di terribili casi limite. 1) Inizia con compressingValue di 1.0 che significa pochissima compressione. Se le dimensioni dell'immagine sono piccole, le immagini finiranno per essere molti più KB del necessario. 2) Se le immagini sono grandi, sarà lento in quanto potrebbe ricomprimersi molte volte per arrivare al di sotto della dimensione di destinazione. 3) Se le immagini sono molto grandi, potrebbe comprimersi fino al punto in cui l'immagine sembra spazzatura. In questi casi sarebbe meglio non riuscire a salvare l'immagine e dire all'utente che è troppo 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!)!
}

9

Jus Fixing per Xcode 7, testato il 21/09/2015 e funzionante:

Basta creare un'estensione UIImagecome segue:

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

Quindi puoi usarlo in questo modo:

let imageData = imagePassed.lowestQualityJPEGNSData

Core ringrazia il proprietario della risposta, il signor Thiago e l'editore, il signor kuldeep
Abhimanyu Rathore,

5

Approccio binario Swift 4 per comprimere l'immagine

Credo che sia abbastanza tardi per rispondere a questa domanda, ma ecco la mia soluzione alla domanda che è ottimizzata sto usando la ricerca binaria per trovare il valore ottimale. Quindi, ad esempio, diciamo che con un normale approccio di sottrazione per raggiungere il 62% sarebbero necessari 38 tentativi di compressione, l'approccio * Ricerca binaria ** raggiungerebbe la soluzione richiesta in max log (100) = circa 7 tentativi.

Tuttavia, vorrei anche informarti che la UIImageJPEGRepresentationfunzione non si comporta in modo lineare soprattutto quando il numero raggiunge vicino a 1. Ecco lo screenshot dove possiamo vedere che l'immagine smette di comprimersi dopo che il valore float è> 0.995. Il comportamento è abbastanza imprevedibile, quindi è meglio avere un buffer delta che gestisca questi casi.

inserisci qui la descrizione dell'immagine

Ecco il codice per questo

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

Perché stai usando un ciclo While e incrementando manualmente una variabile? Basta usare for i in 0...13.
Peter Schorn

3

È molto semplice con l' UIImageestensione

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

usare

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

4
molto lento e sincronizzato
iman kazemayni

1
potrebbe accadere che la tua immagine ridimensionata non sarà mai inferiore a 300kB e non hai alcun fallback per questo caso
slxl

almeno (compressQuality> = 0) potrebbe essere aggiunto al ciclo while come condizione && extra
slxl

2

Aggiornamento Swift 4.2 . Ho creato questa estensione per ridurre le dimensioni di UIImage.
Qui hai due metodi, il primo prende una percentuale e il secondo riduce l'immagine a 1 MB.
Ovviamente puoi cambiare il secondo metodo per diventare 1KB o qualsiasi dimensione tu voglia.

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

In Swift 5 come @Thiago Arreguy risponde:

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 puoi chiamare in questo modo:

let imageData = imagePassed.lowestQualityJPEGNSData

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

2

L'utilizzo func jpegData(compressionQuality: CGFloat) -> Data?funziona bene se non è necessario comprimere a una dimensione specifica. Tuttavia, per alcune immagini, trovo utile poter comprimere al di sotto di una certa dimensione del file. In tal caso, jpegDatanon è affidabile e la compressione iterativa di un'immagine si traduce in un plateau sulla dimensione del file (e può essere davvero costosa). Invece, preferisco ridurre la dimensione dello stesso UIImage, quindi convertirlo in jpegData e controllare se la dimensione ridotta è inferiore al valore che ho scelto (entro un margine di errore che ho impostato). Regolo il moltiplicatore del passo di compressione in base al rapporto tra la dimensione del file corrente e la dimensione del file desiderata per accelerare le prime iterazioni che sono le più costose.

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 utilizzo:

let data = image.compress(to: 300)

L' resizedestensione UIImage proviene da: Come ridimensiono UIImage per ridurre la dimensione dell'immagine caricata


1

Swift 3

Risposta di @ leo-dabus rivista per 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)  }
}

1

In Swift 4 ho creato questa estensione che riceverà la dimensione prevista per tentare di raggiungerla.

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


}

Usare

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

            })
        } catch {
                 print("Error")
        }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.