Bagaimana cara mengompres gambar melalui Javascript di browser?


92

TL; DR;

Apakah ada cara untuk mengompres gambar (kebanyakan jpeg, png dan gif) secara langsung dari sisi browser, sebelum mengunggahnya? Saya cukup yakin JavaScript dapat melakukan ini, tetapi saya tidak dapat menemukan cara untuk mencapainya.


Berikut skenario lengkap yang ingin saya terapkan:

  • pengguna membuka situs web saya, dan memilih gambar melalui input type="file"elemen,
  • gambar ini diambil melalui JavaScript, kami melakukan beberapa verifikasi seperti format file yang benar, ukuran file maksimum dll,
  • jika semuanya baik-baik saja, pratinjau gambar ditampilkan di halaman,
  • pengguna dapat melakukan beberapa operasi dasar seperti memutar gambar sebesar 90 ° / -90 °, memotongnya mengikuti rasio yang telah ditentukan, dll, atau pengguna dapat mengunggah gambar lain dan kembali ke langkah 1,
  • ketika pengguna puas, gambar yang diedit kemudian dikompresi dan "disimpan" secara lokal (tidak disimpan ke file, tetapi di memori / halaman browser), -
  • pengguna mengisi formulir dengan data seperti nama, usia dll,
  • pengguna mengklik tombol "Selesai", kemudian formulir yang berisi data + gambar terkompresi dikirim ke server (tanpa AJAX),

Proses penuh hingga langkah terakhir harus dilakukan di sisi klien, dan harus kompatibel dengan Chrome dan Firefox, Safari 5+, dan IE 8+ terbaru . Jika memungkinkan, hanya JavaScript yang harus digunakan (tapi saya cukup yakin ini tidak mungkin).

Saya belum membuat kode apa pun sekarang, tetapi saya sudah memikirkannya. Pembacaan file secara lokal dimungkinkan melalui File API , pratinjau dan pengeditan gambar dapat dilakukan menggunakan elemen Canvas , tetapi saya tidak dapat menemukan cara untuk melakukan bagian kompresi gambar .

Menurut html5please.com dan caniuse.com , mendukung browser tersebut cukup sulit (terima kasih kepada IE), tapi bisa dilakukan dengan menggunakan polyfill seperti FlashCanvas dan FileReader .

Sebenarnya tujuannya adalah untuk memperkecil ukuran file, jadi saya melihat kompresi gambar sebagai solusi. Tapi, saya tahu bahwa gambar yang diunggah akan ditampilkan di situs web saya, setiap saat di tempat yang sama, dan saya tahu dimensi area tampilan ini (mis. 200x400). Jadi, saya bisa mengubah ukuran gambar agar sesuai dengan dimensi tersebut, sehingga mengurangi ukuran file. Saya tidak tahu berapa rasio kompresi untuk teknik ini.

Bagaimana menurut anda ? Apakah Anda punya saran untuk memberi tahu saya? Apakah Anda tahu cara mengompresi sisi browser gambar di JavaScript? Terima kasih atas balasan Anda.

Jawaban:


166

Pendeknya:

  • Baca file menggunakan API FileReader HTML5 dengan .readAsArrayBuffer
  • Buat Blob dengan data file dan dapatkan url-nya dengan window.URL.createObjectURL (blob)
  • Buat elemen Image baru dan setel srcnya ke file blob url
  • Kirim gambar ke kanvas. Ukuran kanvas disetel ke ukuran keluaran yang diinginkan
  • Dapatkan kembali data yang diperkecil dari kanvas melalui canvas.toDataURL ("image / jpeg", 0.7) (setel format dan kualitas keluaran Anda sendiri)
  • Lampirkan input tersembunyi baru ke formulir asli dan transfer gambar dataURI pada dasarnya sebagai teks biasa
  • Di backend, baca dataURI, dekode dari Base64, dan simpan

Sumber: kode .


1
Terima kasih banyak ! Inilah yang saya cari. Tahukah Anda seberapa bagus rasio kompresi dengan teknik ini?
pomeh

2
@NicholasKyriakides Saya dapat mengonfirmasi bahwa canvas.toDataURL("image/jpeg",0.7)secara efektif mengompresnya, ia menyimpan JPEG dengan kualitas 70 (berbeda dengan default, kualitas 100).
user1111929

4
@Nicholas Kyriakides, ini bukan perbedaan yang baik untuk dibuat. Sebagian besar codec tidak lossless, sehingga akan cocok dengan definisi "downscaling" Anda (misalnya, Anda tidak dapat kembali ke 100).
Billybobbonnet

5
Downscaling mengacu pada membuat gambar dengan ukuran lebih kecil dalam hal tinggi dan lebarnya. Ini benar-benar kompresi. Ini kompresi lossy tetapi tentu saja kompresi. Ini tidak menurunkan skala piksel, itu hanya mendorong beberapa piksel menjadi warna yang sama sehingga kompresi dapat mencapai warna-warna itu dalam bit yang lebih sedikit. JPEG telah membangun kompresi untuk pikselnya, tetapi dalam mode lossy dikatakan bahwa beberapa warna yang mati dapat disebut warna yang sama. Itu masih kompresi. Penurunan skala berkaitan dengan grafik biasanya mengacu pada perubahan ukuran sebenarnya.
Tatarize

3
Saya hanya ingin mengatakan ini: file dapat langsung masuk URL.createObjectUrl()tanpa mengubah file menjadi gumpalan; file tersebut dihitung sebagai gumpalan.
hellol11

20

Saya melihat dua hal yang hilang dari jawaban lain:

  • canvas.toBlob(bila tersedia) lebih berkinerja daripada canvas.toDataURL, dan juga asinkron.
  • file -> gambar -> kanvas -> konversi file kehilangan data EXIF; khususnya, data tentang rotasi gambar yang biasa disetel oleh ponsel / tablet modern.

Skrip berikut membahas kedua poin tersebut:

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

Contoh penggunaan:

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 melakukan trik untuk saya, membuat file dan menerima di array $ _FILES di server. Terima kasih!
Ricardo Ruiz Romero

Tampak hebat! Tetapi apakah ini akan berfungsi di semua browser, web, dan seluler? (Mari kita abaikan IE)
Garvit Jain

14

Jawaban @PsychoWoods bagus. Saya ingin menawarkan solusi saya sendiri. Fungsi Javascript ini mengambil URL dan lebar data gambar, menskalakannya ke lebar baru, dan mengembalikan URL data baru.

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

Kode ini dapat digunakan di mana pun Anda memiliki URL data dan menginginkan URL data untuk gambar yang diperkecil.


tolong, bisakah Anda memberi saya lebih banyak detail tentang contoh ini, bagaimana memanggil fungsi dan bagaimana mengembalikan hasilnya?
visulo

Berikut ini contohnya: danielsadventure.info/Html/scaleimage.html Pastikan untuk membaca sumber halaman untuk melihat cara kerjanya.
Sungai Vivian

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… Untuk orang lain yang ingin membaca link yang diusulkan oleh @DanielAllenLangdon
Cedric Arnould

Sebagai peringatan, terkadang image.width / height akan mengembalikan 0 karena belum dimuat. Anda mungkin perlu mengubahnya menjadi fungsi asinkron dan mendengarkan image.onload untuk mendapatkan gambar yang benar dengan tinggi dan.
Matt Pope


4

Saya mengalami masalah dengan downscaleImage()fungsi yang diposting di atas oleh @ daniel-allen-langdon di mana properti image.widthdan image.heighttidak segera tersedia karena pemuatan gambar tidak sinkron .

Silakan lihat contoh TypeScript yang diperbarui di bawah ini yang mempertimbangkan hal ini, menggunakan asyncfungsi, dan mengubah ukuran gambar berdasarkan dimensi terpanjang, bukan hanya lebarnya

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

}

Saya akan menambahkan penjelasan tentang quality, resolutiondan imageType(format ini)
Barabas

3

Sunting: Sesuai komentar Tuan Me pada jawaban ini, sepertinya kompresi sekarang tersedia untuk format JPG / WebP (lihat https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Sejauh yang saya tahu, Anda tidak dapat mengompres gambar menggunakan kanvas, sebagai gantinya, Anda dapat mengubah ukurannya. Menggunakan canvas.toDataURL tidak akan membiarkan Anda memilih rasio kompresi yang akan digunakan. Anda dapat melihat canimage yang melakukan apa yang Anda inginkan: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

Nyatanya, seringkali cukup mengubah ukuran gambar untuk memperkecil ukurannya tetapi jika Anda ingin melangkah lebih jauh, Anda harus menggunakan metode yang baru diperkenalkan file.readAsArrayBuffer untuk mendapatkan buffer yang berisi data gambar.

Kemudian, cukup gunakan DataView untuk membaca kontennya sesuai dengan spesifikasi format gambar ( http://en.wikipedia.org/wiki/JPEG atau http://en.wikipedia.org/wiki/Portable_Network_Graphics ).

Akan sulit untuk menangani kompresi data gambar, tetapi lebih buruk jika dicoba. Di sisi lain, Anda dapat mencoba untuk menghapus header PNG atau data exif JPEG untuk membuat gambar Anda lebih kecil, seharusnya lebih mudah untuk melakukannya.

Anda harus membuat DataWiew lain di buffer lain dan mengisinya dengan konten gambar yang difilter. Kemudian, Anda hanya perlu menyandikan konten gambar Anda ke DataURI menggunakan window.btoa.

Beri tahu saya jika Anda menerapkan sesuatu yang serupa, akan menarik untuk melewati kode.


Mungkin ada sesuatu yang berubah sejak Anda memposting ini, tetapi argumen kedua ke fungsi canvas.toDataURL yang Anda sebutkan adalah jumlah kompresi yang ingin Anda terapkan.
wp-overwatch.com

2

Saya menemukan bahwa ada solusi yang lebih sederhana dibandingkan dengan jawaban yang diterima.

  • Baca file menggunakan HTML5 FileReader API dengan .readAsArrayBuffer
  • Buat Blob dengan data file dan dapatkan url-nya window.URL.createObjectURL(blob)
  • Buat elemen Image baru dan setel srcnya ke file blob url
  • Kirim gambar ke kanvas. Ukuran kanvas disetel ke ukuran keluaran yang diinginkan
  • Dapatkan kembali data yang diperkecil dari kanvas melalui canvas.toDataURL("image/jpeg",0.7)(atur format dan kualitas keluaran Anda sendiri)
  • Lampirkan input tersembunyi baru ke formulir asli dan transfer gambar dataURI pada dasarnya sebagai teks biasa
  • Di backend, baca dataURI, dekode dari Base64, dan simpan

Sesuai pertanyaan Anda:

Apakah ada cara untuk mengompres gambar (kebanyakan jpeg, png dan gif) secara langsung di sisi browser, sebelum mengunggahnya

Solusi saya:

  1. Buat blob dengan file secara langsung dengan URL.createObjectURL(inputFileElement.files[0]).

  2. Sama seperti jawaban yang diterima.

  3. Sama seperti jawaban yang diterima. Perlu disebutkan bahwa, ukuran kanvas diperlukan dan digunakan img.widthserta img.heightuntuk mengatur canvas.widthdan canvas.height. Tidak img.clientWidth.

  4. Dapatkan gambar yang diperkecil canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Pengaturan 'image/jpg'tidak berpengaruh. image/pngjuga didukung. Buat Fileobjek baru di dalam callbackfunction tubuh dengan let compressedImageBlob = new File([blob]).

  5. Tambahkan input tersembunyi baru atau kirim melalui javascript . Server tidak perlu memecahkan kode apa pun.

Periksa https://javascript.info/binary untuk semua informasi. Saya mendapatkan solusi setelah membaca bab ini.


Kode:

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

Kode ini terlihat jauh lebih menakutkan dibandingkan jawaban lainnya ..

Memperbarui:

Seseorang harus memasukkan semuanya ke dalam img.onload. Jika canvastidak, tidak akan bisa mendapatkan lebar dan tinggi gambar dengan benar sesuai waktu canvasyang ditentukan.

    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 .pnguji kompresi file dengan image/jpegset argumen.

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


0

Saya meningkatkan fungsinya menjadi ini:

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

penggunaan itu:

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

Nikmati ;)

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.