Wie komprimiere ich ein Bild über Javascript im Browser?


89

TL; DR;

Gibt es eine Möglichkeit, ein Bild (meistens JPEG, PNG und GIF) direkt im Browser zu komprimieren, bevor es hochgeladen wird? Ich bin mir ziemlich sicher, dass JavaScript dies kann, aber ich kann keinen Weg finden, dies zu erreichen.


Hier ist das vollständige Szenario, das ich implementieren möchte:

  • Der Benutzer besucht meine Website und wählt ein Bild über ein input type="file"Element aus.
  • Dieses Bild wird über JavaScript abgerufen. Wir führen einige Überprüfungen durch, z. B. das korrekte Dateiformat, die maximale Dateigröße usw.
  • Wenn alles in Ordnung ist, wird auf der Seite eine Vorschau des Bildes angezeigt.
  • Der Benutzer kann einige grundlegende Vorgänge ausführen, z. B. das Bild um 90 ° / -90 ° drehen, es nach einem vordefinierten Verhältnis zuschneiden usw. oder der Benutzer kann ein anderes Bild hochladen und zu Schritt 1 zurückkehren.
  • Wenn der Benutzer zufrieden ist, wird das bearbeitete Bild komprimiert und lokal "gespeichert" (nicht in einer Datei gespeichert, sondern im Speicher / auf der Seite des Browsers).
  • Der Benutzer füllt ein Formular mit Daten wie Name, Alter usw. aus.
  • Wenn der Benutzer auf die Schaltfläche "Fertig stellen" klickt, wird das Formular mit Daten + komprimiertem Bild an den Server gesendet (ohne AJAX).

Der gesamte Prozess bis zum letzten Schritt sollte clientseitig erfolgen und mit den neuesten Versionen von Chrome und Firefox, Safari 5+ und IE 8+ kompatibel sein . Wenn möglich, sollte nur JavaScript verwendet werden (aber ich bin mir ziemlich sicher, dass dies nicht möglich ist).

Ich habe momentan nichts codiert, aber ich habe bereits darüber nachgedacht. Das lokale Lesen von Dateien ist über die Datei-API möglich. Die Bildvorschau und -bearbeitung kann mit dem Canvas- Element erfolgen, aber ich kann keine Möglichkeit finden, den Teil der Bildkomprimierung durchzuführen .

Laut html5please.com und caniuse.com ist die Unterstützung dieses Browsers (dank IE) ziemlich schwierig, könnte aber mit Polyfill wie FlashCanvas und FileReader erfolgen .

Eigentlich ist das Ziel, die Dateigröße zu reduzieren, daher sehe ich die Bildkomprimierung als Lösung. Ich weiß jedoch, dass hochgeladene Bilder jedes Mal am selben Ort auf meiner Website angezeigt werden, und ich kenne die Größe dieses Anzeigebereichs (z. B. 200 x 400). So konnte ich die Größe des Bilds an diese Abmessungen anpassen und so die Dateigröße reduzieren. Ich habe keine Ahnung, wie hoch das Kompressionsverhältnis für diese Technik sein würde.

Was denken Sie ? Hast du einen Rat, den du mir sagen kannst? Kennen Sie eine Möglichkeit, eine Bildbrowserseite in JavaScript zu komprimieren? Vielen Dank für Ihre Antworten.

Antworten:


159

Zusamenfassend:

  • Lesen Sie die Dateien mithilfe der HTML5 FileReader-API mit .readAsArrayBuffer
  • Erstellen Sie einen Blob mit den Dateidaten und rufen Sie seine URL mit window.URL.createObjectURL (blob) ab.
  • Erstellen Sie ein neues Image-Element und setzen Sie dessen src auf die Datei-Blob-URL
  • Senden Sie das Bild auf die Leinwand. Die Leinwandgröße wird auf die gewünschte Ausgabegröße eingestellt
  • Holen Sie sich die verkleinerten Daten von canvas über canvas.toDataURL ("image / jpeg", 0.7) zurück (stellen Sie Ihr eigenes Ausgabeformat und Ihre eigene Qualität ein).
  • Fügen Sie dem ursprünglichen Formular neue versteckte Eingaben hinzu und übertragen Sie die dataURI-Bilder grundsätzlich als normalen Text
  • Lesen Sie im Backend die dataURI, dekodieren Sie sie von Base64 und speichern Sie sie

Quelle: Code .


1
Vielen Dank ! Das habe ich gesucht. Wissen Sie, wie gut das Kompressionsverhältnis mit dieser Technik ist?
Pomeh

2
@NicholasKyriakides Ich kann bestätigen, dass es canvas.toDataURL("image/jpeg",0.7)effektiv komprimiert, es speichert JPEG mit der Qualität 70 (im Gegensatz zur Standardqualität 100).
user1111929

4
@Nicholas Kyriakides, das ist keine gute Unterscheidung. Die meisten Codecs sind nicht verlustfrei, daher passen sie in Ihre "Downscaling" -Definition (dh Sie können nicht auf 100 zurückgreifen).
Billybobbonnet

5
Downscaling bezieht sich auf Bilder, die in Bezug auf Höhe und Breite kleiner sind. Das ist wirklich Komprimierung. Es ist verlustbehaftete Komprimierung, aber sicherlich Komprimierung. Die Pixel werden nicht verkleinert, sondern nur einige der Pixel werden auf dieselbe Farbe gebracht, sodass die Komprimierung diese Farben in weniger Bits treffen kann. JPEG hat ohnehin eine Komprimierung für die Pixel eingebaut, aber im verlustbehafteten Modus heißt es, dass einige abweichende Farben als dieselbe Farbe bezeichnet werden können. Das ist immer noch Komprimierung. Eine Verkleinerung in Bezug auf Grafiken bezieht sich normalerweise auf eine Änderung der tatsächlichen Größe.
Tatarize

3
Ich möchte nur Folgendes sagen: Die Datei kann direkt aufgerufen werden, URL.createObjectUrl()ohne dass die Datei in einen Blob umgewandelt wird. Die Datei zählt als Blob.
hellol11

19

In den anderen Antworten fehlen zwei Dinge:

  • canvas.toBlob(falls verfügbar) ist performanter als canvas.toDataURLund auch asynchron.
  • Die Datei -> Bild -> Leinwand -> Dateikonvertierung verliert EXIF-Daten. insbesondere Daten zur Bildrotation, die üblicherweise von modernen Telefonen / Tablets eingestellt werden.

Das folgende Skript behandelt beide Punkte:

// From https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob, needed for Safari:
if (!HTMLCanvasElement.prototype.toBlob) {
    Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
        value: function(callback, type, quality) {

            var binStr = atob(this.toDataURL(type, quality).split(',')[1]),
                len = binStr.length,
                arr = new Uint8Array(len);

            for (var i = 0; i < len; i++) {
                arr[i] = binStr.charCodeAt(i);
            }

            callback(new Blob([arr], {type: type || 'image/png'}));
        }
    });
}

window.URL = window.URL || window.webkitURL;

// Modified from https://stackoverflow.com/a/32490603, cc by-sa 3.0
// -2 = not jpeg, -1 = no data, 1..8 = orientations
function getExifOrientation(file, callback) {
    // Suggestion from http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/:
    if (file.slice) {
        file = file.slice(0, 131072);
    } else if (file.webkitSlice) {
        file = file.webkitSlice(0, 131072);
    }

    var reader = new FileReader();
    reader.onload = function(e) {
        var view = new DataView(e.target.result);
        if (view.getUint16(0, false) != 0xFFD8) {
            callback(-2);
            return;
        }
        var length = view.byteLength, offset = 2;
        while (offset < length) {
            var marker = view.getUint16(offset, false);
            offset += 2;
            if (marker == 0xFFE1) {
                if (view.getUint32(offset += 2, false) != 0x45786966) {
                    callback(-1);
                    return;
                }
                var little = view.getUint16(offset += 6, false) == 0x4949;
                offset += view.getUint32(offset + 4, little);
                var tags = view.getUint16(offset, little);
                offset += 2;
                for (var i = 0; i < tags; i++)
                    if (view.getUint16(offset + (i * 12), little) == 0x0112) {
                        callback(view.getUint16(offset + (i * 12) + 8, little));
                        return;
                    }
            }
            else if ((marker & 0xFF00) != 0xFF00) break;
            else offset += view.getUint16(offset, false);
        }
        callback(-1);
    };
    reader.readAsArrayBuffer(file);
}

// Derived from https://stackoverflow.com/a/40867559, cc by-sa
function imgToCanvasWithOrientation(img, rawWidth, rawHeight, orientation) {
    var canvas = document.createElement('canvas');
    if (orientation > 4) {
        canvas.width = rawHeight;
        canvas.height = rawWidth;
    } else {
        canvas.width = rawWidth;
        canvas.height = rawHeight;
    }

    if (orientation > 1) {
        console.log("EXIF orientation = " + orientation + ", rotating picture");
    }

    var ctx = canvas.getContext('2d');
    switch (orientation) {
        case 2: ctx.transform(-1, 0, 0, 1, rawWidth, 0); break;
        case 3: ctx.transform(-1, 0, 0, -1, rawWidth, rawHeight); break;
        case 4: ctx.transform(1, 0, 0, -1, 0, rawHeight); break;
        case 5: ctx.transform(0, 1, 1, 0, 0, 0); break;
        case 6: ctx.transform(0, 1, -1, 0, rawHeight, 0); break;
        case 7: ctx.transform(0, -1, -1, 0, rawHeight, rawWidth); break;
        case 8: ctx.transform(0, -1, 1, 0, 0, rawWidth); break;
    }
    ctx.drawImage(img, 0, 0, rawWidth, rawHeight);
    return canvas;
}

function reduceFileSize(file, acceptFileSize, maxWidth, maxHeight, quality, callback) {
    if (file.size <= acceptFileSize) {
        callback(file);
        return;
    }
    var img = new Image();
    img.onerror = function() {
        URL.revokeObjectURL(this.src);
        callback(file);
    };
    img.onload = function() {
        URL.revokeObjectURL(this.src);
        getExifOrientation(file, function(orientation) {
            var w = img.width, h = img.height;
            var scale = (orientation > 4 ?
                Math.min(maxHeight / w, maxWidth / h, 1) :
                Math.min(maxWidth / w, maxHeight / h, 1));
            h = Math.round(h * scale);
            w = Math.round(w * scale);

            var canvas = imgToCanvasWithOrientation(img, w, h, orientation);
            canvas.toBlob(function(blob) {
                console.log("Resized image to " + w + "x" + h + ", " + (blob.size >> 10) + "kB");
                callback(blob);
            }, 'image/jpeg', quality);
        });
    };
    img.src = URL.createObjectURL(file);
}

Anwendungsbeispiel:

inputfile.onchange = function() {
    // If file size > 500kB, resize such that width <= 1000, quality = 0.9
    reduceFileSize(this.files[0], 500*1024, 1000, Infinity, 0.9, blob => {
        let body = new FormData();
        body.set('file', blob, blob.name || "file.jpg");
        fetch('/upload-image', {method: 'POST', body}).then(...);
    });
};

ToBlob hat den Trick für mich gemacht, eine Datei erstellt und im Array $ _FILES auf dem Server empfangen. Danke dir!
Ricardo Ruiz Romero

Sieht großartig aus! Aber funktioniert dies auf allen Browsern, im Internet und auf Mobilgeräten? (Lassen Sie uns IE ignorieren)
Garvit Jain

14

Die Antwort von @PsychoWoods ist gut. Ich möchte meine eigene Lösung anbieten. Diese Javascript-Funktion verwendet eine Bilddaten-URL und eine Breite, skaliert sie auf die neue Breite und gibt eine neue Daten-URL zurück.

// Take an image URL, downscale it to the given width, and return a new image URL.
function downscaleImage(dataUrl, newWidth, imageType, imageArguments) {
    "use strict";
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;

    // Provide default values
    imageType = imageType || "image/jpeg";
    imageArguments = imageArguments || 0.7;

    // Create a temporary image so that we can compute the height of the downscaled image.
    image = new Image();
    image.src = dataUrl;
    oldWidth = image.width;
    oldHeight = image.height;
    newHeight = Math.floor(oldHeight / oldWidth * newWidth)

    // Create a temporary canvas to draw the downscaled image on.
    canvas = document.createElement("canvas");
    canvas.width = newWidth;
    canvas.height = newHeight;

    // Draw the downscaled image on the canvas and return the new data URL.
    ctx = canvas.getContext("2d");
    ctx.drawImage(image, 0, 0, newWidth, newHeight);
    newDataUrl = canvas.toDataURL(imageType, imageArguments);
    return newDataUrl;
}

Dieser Code kann überall dort verwendet werden, wo Sie eine Daten-URL haben und eine Daten-URL für ein verkleinertes Bild wünschen.


Können Sie mir bitte mehr Details zu diesem Beispiel geben, wie man die Funktion aufruft und wie das Ergebnis zurückgegeben wird?
Visulo

Hier ein Beispiel: danielsadventure.info/Html/scaleimage.html Lesen Sie unbedingt die Quelle der Seite, um zu sehen, wie es funktioniert.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Für andere Personen, die den von @DanielAllenLangdon vorgeschlagenen Link lesen möchten
Cedric Arnould

Als Heads-Up gibt image.width / height manchmal 0 zurück, da es nicht geladen wurde. Möglicherweise müssen Sie dies in eine asynchrone Funktion konvertieren und image.onload anhören, um das richtige Bild mit und die richtige Höhe zu erhalten.
Matt Pope


4

Ich hatte ein Problem mit der downscaleImage()oben von @ daniel-allen-langdon geposteten Funktion, da die Eigenschaften image.widthund image.heightnicht sofort verfügbar sind, da das Laden des Bildes asynchron ist .

Das aktualisierte TypeScript-Beispiel unten berücksichtigt dies, verwendet asyncFunktionen und ändert die Größe des Bilds basierend auf der längsten Dimension und nicht nur auf der Breite

function getImage(dataUrl: string): Promise<HTMLImageElement> 
{
    return new Promise((resolve, reject) => {
        const image = new Image();
        image.src = dataUrl;
        image.onload = () => {
            resolve(image);
        };
        image.onerror = (el: any, err: ErrorEvent) => {
            reject(err.error);
        };
    });
}

export async function downscaleImage(
        dataUrl: string,  
        imageType: string,  // e.g. 'image/jpeg'
        resolution: number,  // max width/height in pixels
        quality: number   // e.g. 0.9 = 90% quality
    ): Promise<string> {

    // Create a temporary image so that we can compute the height of the image.
    const image = await getImage(dataUrl);
    const oldWidth = image.naturalWidth;
    const oldHeight = image.naturalHeight;
    console.log('dims', oldWidth, oldHeight);

    const longestDimension = oldWidth > oldHeight ? 'width' : 'height';
    const currentRes = longestDimension == 'width' ? oldWidth : oldHeight;
    console.log('longest dim', longestDimension, currentRes);

    if (currentRes > resolution) {
        console.log('need to resize...');

        // Calculate new dimensions
        const newSize = longestDimension == 'width'
            ? Math.floor(oldHeight / oldWidth * resolution)
            : Math.floor(oldWidth / oldHeight * resolution);
        const newWidth = longestDimension == 'width' ? resolution : newSize;
        const newHeight = longestDimension == 'height' ? resolution : newSize;
        console.log('new width / height', newWidth, newHeight);

        // Create a temporary canvas to draw the downscaled image on.
        const canvas = document.createElement('canvas');
        canvas.width = newWidth;
        canvas.height = newHeight;

        // Draw the downscaled image on the canvas and return the new data URL.
        const ctx = canvas.getContext('2d')!;
        ctx.drawImage(image, 0, 0, newWidth, newHeight);
        const newDataUrl = canvas.toDataURL(imageType, quality);
        return newDataUrl;
    }
    else {
        return dataUrl;
    }

}

Ich würde die Erklärung hinzufügen quality, resolutionund imageType(Format dieser)
Barabas

3

Bearbeiten: Laut dem Kommentar von Mr Me zu dieser Antwort scheint die Komprimierung jetzt für JPG / WebP-Formate verfügbar zu sein (siehe https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ). .

Soweit ich weiß, können Sie Bilder nicht mit Leinwand komprimieren, sondern die Größe ändern. Wenn Sie canvas.toDataURL verwenden, können Sie das zu verwendende Komprimierungsverhältnis nicht auswählen. Sie können sich canimage ansehen, das genau das tut, was Sie wollen: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

Tatsächlich reicht es oft aus, nur die Größe des Bildes zu ändern, um seine Größe zu verringern. Wenn Sie jedoch weiter gehen möchten, müssen Sie die neu eingeführte Methode file.readAsArrayBuffer verwenden, um einen Puffer mit den Bilddaten abzurufen.

Verwenden Sie dann einfach eine DataView, um den Inhalt gemäß der Bildformatspezifikation zu lesen ( http://en.wikipedia.org/wiki/JPEG oder http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Es wird schwierig sein, mit der Komprimierung von Bilddaten umzugehen, aber es ist schlimmer, es zu versuchen. Auf der anderen Seite können Sie versuchen, die PNG-Header oder die JPEG-Exif-Daten zu löschen, um Ihr Bild zu verkleinern. Dies sollte einfacher sein.

Sie müssen eine weitere DataWiew in einem anderen Puffer erstellen und diese mit dem gefilterten Bildinhalt füllen. Dann müssen Sie nur noch Ihren Bildinhalt mit window.btoa in DataURI codieren.

Lassen Sie mich wissen, wenn Sie etwas Ähnliches implementieren, wird es interessant sein, den Code durchzugehen.


Vielleicht hat sich etwas geändert, seit Sie dies gepostet haben, aber das zweite Argument für die von Ihnen erwähnte Funktion canvas.toDataURL ist der Grad der Komprimierung, den Sie anwenden möchten.
wp-overwatch.com


0

Ich habe die Funktion eines Kopfes so verbessert:

var minifyImg = function(dataUrl,newWidth,imageType="image/jpeg",resolve,imageArguments=0.7){
    var image, oldWidth, oldHeight, newHeight, canvas, ctx, newDataUrl;
    (new Promise(function(resolve){
      image = new Image(); image.src = dataUrl;
      log(image);
      resolve('Done : ');
    })).then((d)=>{
      oldWidth = image.width; oldHeight = image.height;
      log([oldWidth,oldHeight]);
      newHeight = Math.floor(oldHeight / oldWidth * newWidth);
      log(d+' '+newHeight);

      canvas = document.createElement("canvas");
      canvas.width = newWidth; canvas.height = newHeight;
      log(canvas);
      ctx = canvas.getContext("2d");
      ctx.drawImage(image, 0, 0, newWidth, newHeight);
      //log(ctx);
      newDataUrl = canvas.toDataURL(imageType, imageArguments);
      resolve(newDataUrl);
    });
  };

die Verwendung davon:

minifyImg(<--DATAURL_HERE-->,<--new width-->,<--type like image/jpeg-->,(data)=>{
   console.log(data); // the new DATAURL
});

genießen ;)


0

Ich finde, dass es eine einfachere Lösung gibt als die akzeptierte Antwort.

  • Lesen Sie die Dateien mit der HTML5 FileReader-API mit .readAsArrayBuffer
  • Erstellen Sie einen Blob mit den Dateidaten und erhalten Sie die URL mit window.URL.createObjectURL(blob)
  • Erstellen Sie ein neues Image-Element und setzen Sie dessen src auf die Datei-Blob-URL
  • Senden Sie das Bild auf die Leinwand. Die Leinwandgröße wird auf die gewünschte Ausgabegröße eingestellt
  • Holen Sie sich die verkleinerten Daten von der Leinwand zurück über canvas.toDataURL("image/jpeg",0.7)(stellen Sie Ihr eigenes Ausgabeformat und Ihre eigene Qualität ein)
  • Fügen Sie dem ursprünglichen Formular neue versteckte Eingaben hinzu und übertragen Sie die dataURI-Bilder grundsätzlich als normalen Text
  • Lesen Sie im Backend die dataURI, dekodieren Sie sie von Base64 und speichern Sie sie

Gemäß Ihrer Frage:

Gibt es eine Möglichkeit, ein Bild (meistens JPEG, PNG und GIF) direkt im Browser zu komprimieren, bevor es hochgeladen wird?

Meine Lösung:

  1. Erstellen Sie einen Blob mit der Datei direkt mit URL.createObjectURL(inputFileElement.files[0]).

  2. Gleich wie akzeptierte Antwort.

  3. Gleich wie akzeptierte Antwort. Erwähnenswert ist, dass Leinwandgröße notwendig ist und zu verwenden img.widthund img.heightzu setzen canvas.widthund canvas.height. Nicht img.clientWidth.

  4. Holen Sie sich das verkleinerte Bild von canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Die Einstellung 'image/jpg'hat keine Auswirkung. image/pngwird ebenfalls unterstützt. Machen Sie ein neues FileObjekt im Innern des callbackfunction Körpers mit let compressedImageBlob = new File([blob]).

  5. Fügen Sie neue versteckte Eingaben hinzu oder senden Sie sie über Javascript . Der Server muss nichts dekodieren.

Überprüfen Sie https://javascript.info/binary auf alle Informationen. Nachdem ich dieses Kapitel gelesen hatte, kam ich auf die Lösung.


Code:

    <!DOCTYPE html>
    <html>
    <body>
    <form action="upload.php" method="post" enctype="multipart/form-data">
      Select image to upload:
      <input type="file" name="fileToUpload" id="fileToUpload" multiple>
      <input type="submit" value="Upload Image" name="submit">
    </form>
    </body>
    </html>

Dieser Code sieht weit weniger beängstigend aus als die anderen Antworten.

Aktualisieren:

Man muss alles hineinstecken img.onload. Andernfalls canvaskann die Breite und Höhe des Bildes nicht korrekt ermittelt werden, wenn die Zeit canvaszugewiesen wird.

    function upload(){
        var f = fileToUpload.files[0];
        var fileName = f.name.split('.')[0];
        var img = new Image();
        img.src = URL.createObjectURL(f);
        img.onload = function(){
            var canvas = document.createElement('canvas');
            canvas.width = img.width;
            canvas.height = img.height;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);
            canvas.toBlob(function(blob){
                    console.info(blob.size);
                    var f2 = new File([blob], fileName + ".jpeg");
                    var xhr = new XMLHttpRequest();
                    var form = new FormData();
                    form.append("fileToUpload", f2);
                    xhr.open("POST", "upload.php");
                    xhr.send(form);
            }, 'image/jpeg', 0.5);
        }
    }

3.4MB .pngDateikomprimierungstest mit image/jpeggesetztem Argument.

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |
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.