Wie kann ich ein Bild komprimieren oder verkleinern, bevor ich es als PFFile auf Parse hochlade? (Schnell)


83

Ich habe versucht, eine Bilddatei auf Parse hochzuladen, nachdem ich das Foto direkt am Telefon aufgenommen habe. Aber es wirft eine Ausnahme:

Beenden der App aufgrund der nicht erfassten Ausnahme 'NSInvalidArgumentException', Grund: 'PFFile darf nicht größer als 10485760 Byte sein'

Hier ist mein Code:

In der ersten Ansicht 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, der das Bild hochlädt:

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

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

Aber ich muss das Foto noch auf Parse hochladen. Gibt es eine Möglichkeit, die Größe oder Auflösung des Bildes zu reduzieren?


Ich habe den letzten Satz von gbk und fount am Ende ausprobiert: Wenn ich let newData = UIImageJPEGRepresentation (UIImage (data: data), 1) aufrufe, ist newData.count nicht gleich data.count und mit einem Faktor von mehr als wirklich größer 2. Was für mich wirklich überraschend ist! Trotzdem danke für den Code!
NicoD

Antworten:


182

Ja, Sie können UIImageJPEGRepresentationanstelle von verwenden UIImagePNGRepresentation, um die Größe Ihrer Bilddatei zu reduzieren. Sie können einfach eine Erweiterung UIImage wie folgt erstellen:

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

bearbeiten / aktualisieren:

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

Verwendung:

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

1
Verwendung von nicht deklariertem Typ UIImage. Dies ist der Fehler, den ich bekomme
Umit Kaya

1
Ich habe Bilder mit einer Größe von 22-25 MB zurückgegeben, jetzt ein Bruchteil davon. Ich danke dir sehr! Tolle Erweiterung!
Octavio Antonio Cedeño

2
Es gibt keinen anderen Unterschied als die Syntax. Natürlich können Sie den Code manuell schreiben, UIImageJPEGRepresentation(yourImage, 1.0)anstatt nur zu tippen, .jpund xcode die Methode für Sie automatisch vervollständigen lassen. Gleiches gilt für die Komprimierungsaufzählung .whatever.
Leo Dabus

1
@ Umitk sollten Sie UIKit
Alan

1
'jpegData (compressQuality :)' wurde in 'UIImageJPEGRepresentation ( : :)' umbenannt
5uper_0leh

51

Wenn Sie die Bildgröße auf einen konkreten Wert beschränken möchten, können Sie Folgendes tun:

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

Dies ist eine umfassendere Lösung.
Idrees Ashraf

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

13
Das ist sehr teuer, du machst eine so schwere Aufgabe in einer while-Schleife und jedes Mal !! Keine einschränkenden Bedingungen.
Amber K

Umfassend, aber sehr schwer im Gedächtnis. Dies stürzt ältere Geräte mit Speicherproblemen ab. Es muss einen günstigeren Weg geben, dies zu tun.
Tofu Warrior

1
Dies ist eine wirklich schlechte Idee, es ist voller schrecklicher Randfälle. 1) Es beginnt mit einem Komprimierungswert von 1,0, was kaum eine Komprimierung bedeutet. Wenn die Bildabmessungen klein sind, sind die Bilder viel mehr KB als nötig. 2) Wenn die Bilder groß sind, ist es langsam, da es viele Male neu komprimiert werden kann, um unter die Zielgröße zu gelangen. 3) Wenn die Bilder sehr groß sind, wird sie möglicherweise bis zu dem Punkt komprimiert, an dem das Bild wie Müll aussieht. In diesen Fällen ist es besser, das Bild einfach nicht zu speichern und dem Benutzer mitzuteilen, dass es zu groß ist.
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 für Xcode 7, getestet am 21.09.2015 und funktioniert einwandfrei:

Erstellen Sie einfach eine Erweiterung UIImagewie folgt:

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

Dann können Sie es so verwenden:

let imageData = imagePassed.lowestQualityJPEGNSData

Kern Dank an Antwort Inhaber Herr Thiago & Herausgeber Herr Kuldeep
Abhimanyu Rathore

6

Swift 4 Binary Approach zum Komprimieren von Bildern

Ich glaube, es ist ziemlich spät, diese Frage zu beantworten, aber hier ist meine Lösung für die Frage, die optimiert ist. Ich verwende die binäre Suche , um den optimalen Wert zu finden. Wenn beispielsweise ein normaler Subtraktionsansatz zum Erreichen von 62% 38 Komprimierungsversuche erfordern würde, würde der Ansatz der * binären Suche ** die erforderliche Lösung in max log (100) = etwa 7 Versuchen erreichen.

Ich möchte Sie jedoch auch darüber informieren, dass sich die UIImageJPEGRepresentationFunktion nicht linear verhält, insbesondere wenn die Zahl nahe 1 liegt. Hier ist der Screenshot, in dem wir sehen können, dass das Bild nicht mehr komprimiert wird, nachdem der Gleitkommawert> 0,995 ist. Das Verhalten ist ziemlich unvorhersehbar, daher ist es besser, einen Delta-Puffer zu haben, der solche Fälle behandelt.

Geben Sie hier die Bildbeschreibung ein

Hier ist der Code dafür

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

Warum verwenden Sie eine While-Schleife und erhöhen eine Variable manuell? Einfach benutzen for i in 0...13.
Peter Schorn

2

Das ist mit UIImageErweiterung sehr einfach

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

benutzen

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

3
sehr langsam und synchron
iman kazemayni

1
Es kann vorkommen, dass Ihr Bild in der Größe nie kleiner als 300 KB wird und Sie für diesen Fall keinen Fallback haben
slxl

Mindestens (compressQuality> = 0) kann als zusätzliche && Bedingung zur while-Schleife hinzugefügt werden
slxl

2

Swift 4.2 Update . Ich habe diese Erweiterung erstellt, um die UIImage-Größe zu reduzieren.
Hier haben Sie zwei Methoden, die erste nimmt einen Prozentsatz und die zweite reduziert das Bild auf 1 MB.
Natürlich können Sie die zweite Methode in 1 KB oder eine beliebige Größe ändern.

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 als @Thiago Arreguy Antwort:

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

}

Und Sie können so anrufen:

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

1

Swift 3

@ Leo-Dabus Antwort für schnelle 3 überarbeitet

    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 habe ich diese Erweiterung erstellt, die die erwartete Größe erhält und versucht, sie zu erreichen.

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


}

Benutzen

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

            })
        } catch {
                 print("Error")
        }

0

Die Verwendung func jpegData(compressionQuality: CGFloat) -> Data?funktioniert gut, wenn Sie nicht auf eine bestimmte Größe komprimieren müssen. Für bestimmte Bilder finde ich es jedoch nützlich, unter einer bestimmten Dateigröße komprimieren zu können. In diesem Fall,jpegData ist dies unzuverlässig, und das iterative Komprimieren eines Bildes führt zu einem Plateau der Dateigröße (und kann sehr teuer sein). Stattdessen ziehe ich es vor, die Größe des UIImage selbst zu reduzieren, dann in jpegData zu konvertieren und zu überprüfen, ob die reduzierte Größe unter dem von mir gewählten Wert liegt (innerhalb einer von mir festgelegten Fehlergrenze). Ich passe den Multiplikator für den Komprimierungsschritt basierend auf dem Verhältnis der aktuellen Dateigröße zur gewünschten Dateigröße an, um die ersten Iterationen zu beschleunigen, die am teuersten sind.

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

Und Verwendung:

let data = image.compress(to: 300)

Die UIImage- resizedErweiterung stammt von: Wie kann ich die Größe des UIImage ändern, um die Größe des Upload-Bildes zu verringern?

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.