Jak skompresować obraz za pomocą JavaScript w przeglądarce?


91

TL; DR;

Czy istnieje sposób na skompresowanie obrazu (głównie jpeg, png i gif) bezpośrednio po stronie przeglądarki przed przesłaniem? Jestem prawie pewien, że JavaScript może to zrobić, ale nie mogę znaleźć sposobu, aby to osiągnąć.


Oto pełny scenariusz, który chciałbym wdrożyć:

  • użytkownik przechodzi na moją stronę i wybiera obrazek za pomocą input type="file"elementu,
  • ten obraz jest pobierany przez JavaScript, przeprowadzamy weryfikacje, takie jak poprawny format pliku, maksymalny rozmiar pliku itp.
  • jeśli wszystko jest w porządku, na stronie wyświetlany jest podgląd obrazu,
  • użytkownik może wykonać kilka podstawowych operacji, takich jak obrót obrazu o 90 ° / -90 °, przycięcie go zgodnie z wcześniej zdefiniowanym współczynnikiem itp. lub może załadować inny obraz i powrócić do kroku 1,
  • gdy użytkownik jest usatysfakcjonowany, edytowany obraz jest następnie kompresowany i „zapisywany” lokalnie (nie zapisywany do pliku, ale w pamięci / na stronie przeglądarki), -
  • użytkownik wypełnia formularz danymi takimi jak imię i nazwisko, wiek itp.,
  • użytkownik klika w przycisk „Zakończ”, po czym na serwer wysyłany jest formularz zawierający dane + skompresowany obraz (bez AJAX),

Cały proces aż do ostatniego kroku powinien być wykonany po stronie klienta i powinien być zgodny z najnowszymi przeglądarkami Chrome i Firefox, Safari 5+ i IE 8+ . Jeśli to możliwe, należy używać tylko JavaScript (ale jestem pewien, że nie jest to możliwe).

W tej chwili nic nie kodowałem, ale już o tym myślałem. Lokalne odczytywanie plików jest możliwe przez File API , podgląd i edycję obrazu można wykonać za pomocą elementu Canvas , ale nie mogę znaleźć sposobu na wykonanie części kompresji obrazu .

Według html5please.com i caniuse.com obsługa tych przeglądarek jest dość trudna (dzięki IE), ale można by to zrobić za pomocą polyfill, takiego jak FlashCanvas i FileReader .

Właściwie celem jest zmniejszenie rozmiaru pliku, więc uważam kompresję obrazu za rozwiązanie. Ale wiem, że wgrane obrazy będą się wyświetlać na mojej stronie za każdym razem w tym samym miejscu i znam wymiary tego obszaru wyświetlania (np. 200x400). Mogłem więc zmienić rozmiar obrazu, aby pasował do tych wymiarów, zmniejszając w ten sposób rozmiar pliku. Nie mam pojęcia, jaki byłby współczynnik kompresji dla tej techniki.

Co myślisz ? Czy masz jakieś rady, które możesz mi powiedzieć? Czy znasz jakiś sposób na skompresowanie obrazu po stronie przeglądarki w JavaScript? Dziękuję za twoje odpowiedzi.

Odpowiedzi:


164

W skrócie:

  • Odczytaj pliki za pomocą interfejsu API HTML5 FileReader z rozszerzeniem .readAsArrayBuffer
  • Utwórz obiekt Blob z danymi pliku i uzyskaj jego adres URL za pomocą window.URL.createObjectURL (blob)
  • Utwórz nowy element Image i ustaw jego src na adres URL pliku blob
  • Wyślij obraz na płótno. Rozmiar płótna jest ustawiony na żądany rozmiar wyjściowy
  • Pobierz przeskalowane dane z powrotem z kanwy przez canvas.toDataURL ("image / jpeg", 0.7) (ustaw własny format wyjściowy i jakość)
  • Dołącz nowe ukryte dane wejściowe do oryginalnego formularza i prześlij obrazy dataURI w zasadzie jako zwykły tekst
  • W zapleczu przeczytaj dataURI, zdekoduj z Base64 i zapisz go

Źródło: kod .


1
Wielkie dzięki ! To jest to, czego szukałem. Czy wiesz, jak dobry jest współczynnik kompresji w przypadku tej techniki?
pomeh

2
@NicholasKyriakides Mogę potwierdzić, że canvas.toDataURL("image/jpeg",0.7)skutecznie kompresuje, zapisuje JPEG z jakością 70 (w przeciwieństwie do domyślnej jakości 100).
user1111929

4
@Nicholas Kyriakides, to nie jest dobre rozróżnienie. Większość kodeków nie jest bezstratnych, więc pasowałyby do twojej definicji „downscaling” (tj. Nie możesz powrócić do 100).
Billybobbonnet

5
Zmniejszanie odnosi się do tworzenia obrazów o mniejszym rozmiarze pod względem wysokości i szerokości. To naprawdę jest kompresja. To kompresja stratna, ale z pewnością kompresja. Nie zmniejsza skali pikseli, po prostu przesuwa niektóre piksele do tego samego koloru, aby kompresja mogła uderzyć te kolory w mniejszej liczbie bitów. JPEG i tak ma wbudowaną kompresję pikseli, ale w trybie stratnym mówi, że kilka wyłączonych kolorów można nazwać tym samym kolorem. To wciąż kompresja. Zmniejszenie rozmiaru grafiki zwykle odnosi się do zmiany rzeczywistego rozmiaru.
Tatarize

3
Chcę tylko powiedzieć: plik może przejść bezpośrednio do URL.createObjectUrl()pliku bez przekształcania go w obiekt typu blob; plik liczy się jako obiekt blob.
hellol11

20

W pozostałych odpowiedziach brakuje dwóch rzeczy:

  • canvas.toBlob(jeśli jest dostępny) jest bardziej wydajny niż canvas.toDataURL, a także asynchroniczny.
  • plik -> obraz -> płótno -> konwersja pliku traci dane EXIF; w szczególności dane o rotacji obrazu powszechnie ustawiane przez nowoczesne telefony / tablety.

Poniższy skrypt dotyczy obu punktów:

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

Przykładowe użycie:

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 załatwił sprawę, tworząc plik i odbierając go w tablicy $ _FILES na serwerze. Dziękuję Ci!
Ricardo Ruiz Romero

Wygląda świetnie! Ale czy to zadziała we wszystkich przeglądarkach internetowych i mobilnych? (Zignorujmy IE)
Garvit Jain

14

Odpowiedź @PsychoWoods jest dobra. Chciałbym zaproponować własne rozwiązanie. Ta funkcja JavaScript pobiera adres URL danych obrazu i szerokość, skaluje je do nowej szerokości i zwraca nowy adres URL danych.

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

Ten kod może być używany w dowolnym miejscu, w którym masz adres URL danych i potrzebujesz adresu URL danych dla zmniejszonego obrazu.


proszę, czy możesz mi podać więcej szczegółów na temat tego przykładu, jak wywołać funkcję i jak zwrócił wynik?
visulo

Oto przykład: danielsadventure.info/Html/scaleimage.html Przeczytaj źródło strony, aby zobaczyć, jak to działa.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Dla innych osób, które chcą przeczytać link zaproponowany przez @DanielAllenLangdon
Cedric Arnould

Jako ostrzeżenie, czasami image.width / height zwraca 0, ponieważ nie został załadowany. Może być konieczne przekonwertowanie tego na funkcję asynchroniczną i odsłuchanie pliku image.onload, aby uzyskać prawidłowy obraz z i wysokością.
Matt Pope


4

Miałem problem z downscaleImage()funkcją opublikowaną powyżej przez @ daniel-allen-langdon, ponieważ właściwości image.widthi image.heightnie są dostępne natychmiast, ponieważ ładowanie obrazu jest asynchroniczne .

Zobacz zaktualizowany przykład TypeScript poniżej, który bierze to pod uwagę, używa asyncfunkcji i zmienia rozmiar obrazu na podstawie najdłuższego wymiaru, a nie tylko szerokości

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

}

Dodam wyjaśnienie quality, resolutioni imageType(format to)
Barabas

3

Edycja: zgodnie z komentarzem Mr Me do tej odpowiedzi wygląda na to, że kompresja jest teraz dostępna dla formatów JPG / WebP (patrz https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

O ile wiem, nie można kompresować obrazów za pomocą płótna, zamiast tego można zmienić jego rozmiar. Korzystanie z pliku canvas.toDataURL nie pozwoli Ci wybrać współczynnika kompresji. Możesz rzucić okiem na canimage, który robi dokładnie to, co chcesz: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

W rzeczywistości często wystarczy zmienić rozmiar obrazu, aby zmniejszyć jego rozmiar, ale jeśli chcesz pójść dalej, będziesz musiał użyć nowo wprowadzonej metody file.readAsArrayBuffer, aby uzyskać bufor zawierający dane obrazu.

Następnie wystarczy użyć DataView, aby odczytać jego zawartość zgodnie ze specyfikacją formatu obrazu ( http://en.wikipedia.org/wiki/JPEG lub http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Trudno będzie sobie poradzić z kompresją danych obrazu, ale jest to gorsza próba. Z drugiej strony możesz spróbować usunąć nagłówki PNG lub dane JPEG exif, aby zmniejszyć rozmiar obrazu, powinno to być łatwiejsze.

Będziesz musiał utworzyć kolejny DataWiew w innym buforze i wypełnić go przefiltrowaną zawartością obrazu. Następnie wystarczy zakodować zawartość obrazu do DataURI za pomocą window.btoa.

Daj mi znać, jeśli zaimplementujesz coś podobnego, ciekawie będzie przejść przez kod.


Być może coś się zmieniło od czasu, gdy to opublikowałeś, ale drugim argumentem tej funkcji canvas.toDataURL , o którym wspomniałeś, jest wielkość kompresji, którą chcesz zastosować.
wp-overwatch.com

2

Uważam, że istnieje prostsze rozwiązanie w porównaniu z akceptowaną odpowiedzią.

  • Odczytaj pliki za pomocą interfejsu API HTML5 FileReader z .readAsArrayBuffer
  • Utwórz obiekt Blob z danymi pliku i pobierz jego adres URL za pomocą window.URL.createObjectURL(blob)
  • Utwórz nowy element Image i ustaw jego src na adres URL pliku blob
  • Wyślij obraz na płótno. Rozmiar płótna jest ustawiony na żądany rozmiar wyjściowy
  • Uzyskaj skalowane w dół dane z powrotem z kanwy za pośrednictwem canvas.toDataURL("image/jpeg",0.7)(ustaw własny format wyjściowy i jakość)
  • Dołącz nowe ukryte dane wejściowe do oryginalnego formularza i prześlij obrazy dataURI w zasadzie jako zwykły tekst
  • W zapleczu przeczytaj dataURI, zdekoduj z Base64 i zapisz go

Jak na twoje pytanie:

Czy istnieje sposób na skompresowanie obrazu (głównie jpeg, png i gif) bezpośrednio po stronie przeglądarki, przed przesłaniem go

Moje rozwiązanie:

  1. Utwórz obiekt BLOB z plikiem bezpośrednio za pomocą URL.createObjectURL(inputFileElement.files[0]).

  2. Taka sama, jak zaakceptowana odpowiedź.

  3. Taka sama, jak zaakceptowana odpowiedź. Warto wspomnieć, że rozmiar płótna jest niezbędny i używany img.widthoraz img.heightdo ustawienia canvas.widthi canvas.height. Nie img.clientWidth.

  4. Uzyskaj skalowany obraz według canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Ustawienie 'image/jpg'nie ma wpływu. image/pngjest również obsługiwany. Utwórz nowy Fileobiekt wewnątrz callbackfunction ciała za pomocą let compressedImageBlob = new File([blob]).

  5. Dodaj nowe ukryte dane wejściowe lub wyślij przez JavaScript . Serwer nie musi niczego dekodować.

Sprawdź https://javascript.info/binary, aby uzyskać wszystkie informacje. Po przeczytaniu tego rozdziału znalazłem rozwiązanie.


Kod:

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

Ten kod wygląda znacznie mniej przerażająco niż inne odpowiedzi.

Aktualizacja:

Wszystko trzeba włożyć do środka img.onload. W przeciwnym razie canvasnie będzie w stanie uzyskać poprawnej szerokości i wysokości obrazu w canvaswyznaczonym czasie .

    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 .pngtest kompresji plików z image/jpegustawionym argumentem.

    |0.9| 777KB |
    |0.8| 383KB |
    |0.7| 301KB |
    |0.6| 251KB |
    |0.5| 219kB |


0

poprawiłem funkcję głowy, aby była taka:

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

wykorzystanie tego:

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

cieszyć się ;)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.