Tarayıcıda Javascript ile bir resim nasıl sıkıştırılır?


92

TL; DR;

Bir resmi (çoğunlukla jpeg, png ve gif) yüklemeden önce doğrudan tarayıcı tarafında sıkıştırmanın bir yolu var mı? JavaScript'in bunu yapabileceğinden oldukça eminim, ancak bunu başarmanın bir yolunu bulamıyorum.


İşte uygulamak istediğim tam senaryo:

  • kullanıcı web siteme gider ve bir input type="file"öğe aracılığıyla bir resim seçer ,
  • bu görüntü JavaScript aracılığıyla alınır, doğru dosya biçimi, maksimum dosya boyutu vb. gibi bazı doğrulamalar yaparız,
  • her şey yolundaysa, sayfada resmin bir önizlemesi görüntülenir,
  • kullanıcı, resmi 90 ° / -90 ° döndürmek, önceden tanımlanmış bir orana göre kırpmak gibi bazı temel işlemleri yapabilir veya kullanıcı başka bir resim yükleyip 1. adıma geri dönebilir,
  • kullanıcı memnun kaldığında, düzenlenen görüntü daha sonra sıkıştırılır ve yerel olarak "kaydedilir" (bir dosyaya kaydedilmez, ancak tarayıcı belleğine / sayfaya kaydedilir), -
  • kullanıcı adı, yaşı vb. verilerle bir formu doldurur,
  • kullanıcı "Bitir" düğmesine tıklar, ardından verileri + sıkıştırılmış resmi içeren form sunucuya gönderilir (AJAX olmadan),

Son adıma kadar olan tüm süreç istemci tarafında yapılmalı ve en son Chrome ve Firefox, Safari 5+ ve IE 8+ ile uyumlu olmalıdır . Mümkünse, yalnızca JavaScript kullanılmalıdır (ancak bunun mümkün olmadığından oldukça eminim).

Şu anda hiçbir şeyi kodlamadım, ama bunu zaten düşündüm. Dosya API'si aracılığıyla yerel olarak dosya okumak mümkündür , görüntü önizleme ve düzenleme Canvas öğesi kullanılarak yapılabilir , ancak görüntü sıkıştırma bölümünü yapmanın bir yolunu bulamıyorum .

Göre html5please.com ve caniuse.com , bu tarayıcıyı desteklemesini oldukça zor (IE) için teşekkürler, ancak olarak polyfill böyle kullanılarak yapılabilir FlashCanvas ve FileReader .

Aslında amaç dosya boyutunu küçültmek, dolayısıyla görüntü sıkıştırmayı bir çözüm olarak görüyorum. Ancak, yüklenen görüntülerin web sitemde her seferinde aynı yerde gösterileceğini biliyorum ve bu görüntüleme alanının boyutunu biliyorum (örn. 200x400). Böylece, görüntüyü bu boyutlara uyacak şekilde yeniden boyutlandırabilir ve böylece dosya boyutunu azaltabilirim. Bu teknik için sıkıştırma oranının ne olacağı hakkında hiçbir fikrim yok.

Ne düşünüyorsun ? Bana söylemen gereken herhangi bir tavsiyen var mı? JavaScript'te bir görüntü tarayıcı tarafını sıkıştırmanın herhangi bir yolunu biliyor musunuz? Cevaplarınız için teşekkürler.

Yanıtlar:


165

Kısacası:

  • .ReadAsArrayBuffer ile HTML5 FileReader API kullanarak dosyaları okuyun
  • Dosya verileriyle bir Blob oluşturun ve url'sini window.URL.createObjectURL (blob) ile alın
  • Yeni Görüntü öğesi oluşturun ve src dosyasını blob url dosyasına ayarlayın
  • Resmi tuvale gönderin. Tuval boyutu istenen çıktı boyutuna ayarlanır
  • Canvas.toDataURL ("image / jpeg", 0.7) aracılığıyla küçültülmüş verileri tuvalden geri alın (kendi çıktı biçiminizi ve kalitenizi ayarlayın)
  • Orijinal forma yeni gizli girişler ekleyin ve dataURI görüntülerini temelde normal metin olarak aktarın
  • Arka uçta dataURI'yi okuyun, Base64'ten kodunu çözün ve kaydedin

Kaynak: kod .


1
Çok teşekkürler ! Aradığım buydu. Bu teknikle sıkıştırma oranının ne kadar iyi olduğunu biliyor musunuz?
pomeh

2
@NicholasKyriakides canvas.toDataURL("image/jpeg",0.7)Etkili bir şekilde sıkıştırdığını onaylayabilirim, JPEG kalitesinde 70 kaydeder (varsayılanın aksine, kalite 100).
user1111929

4
@Nicholas Kyriakides, bu iyi bir ayrım değil. Codec bileşenlerinin çoğu kayıpsız değildir, bu yüzden "ölçek küçültme" tanımınıza uyacaktır (yani 100'e geri dönemezsiniz).
Billybobbonnet

5
Ölçeği küçültme, görüntüleri yükseklik ve genişlik açısından daha küçük boyutta yapmak anlamına gelir. Bu gerçekten sıkıştırma. Kayıplı sıkıştırma ama kesinlikle sıkıştırma. Piksellerin ölçeğini küçültmez, sadece bazı pikselleri aynı renk olacak şekilde iter, böylece sıkıştırma bu renklere daha az bitte ulaşabilir. JPEG, pikseller için sıkıştırmayı zaten oluşturmuştur, ancak kayıplı modda birkaç rengin aynı renk olarak adlandırılabileceğini söyler. Bu hala sıkıştırma. Grafiklere göre ölçek küçültme, tipik olarak gerçek boyuttaki bir değişikliği ifade eder.
Tatarize

3
Sadece şunu söylemek istiyorum: URL.createObjectUrl()dosyayı bir blob'a dönüştürmeden dosyaya doğrudan gidebilir ; dosya bir blob olarak sayılır.
hellol11

20

Diğer cevaplarda eksik olan iki şey görüyorum:

  • canvas.toBlob(mevcut olduğunda) daha yüksek performanslı canvas.toDataURLve aynı zamanda zaman uyumsuzdur.
  • dosya -> resim -> tuval -> dosya dönüştürme EXIF ​​verilerini kaybeder; özellikle, görüntü döndürmeyle ilgili veriler genellikle modern telefonlar / tabletler tarafından belirlenir.

Aşağıdaki komut dosyası her iki noktayı da ele almaktadır:

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

Örnek kullanım:

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 benim için hile yaptı, bir dosya oluşturdu ve sunucudaki $ _FILES dizisini aldı. Teşekkür ederim!
Ricardo Ruiz Romero

Harika görünüyor! Ancak bu, tüm tarayıcılarda, web'de ve mobil cihazlarda çalışacak mı? (IE'yi görmezden gelelim)
Garvit Jain

14

@PsychoWoods'un cevabı güzel. Kendi çözümümü sunmak istiyorum. Bu Javascript işlevi, bir görüntü verisi URL'sini ve bir genişliği alır, bunu yeni genişliğe ölçeklendirir ve yeni bir veri URL'si döndürür.

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

Bu kod, bir veri URL'niz olan ve ölçeği küçültülmüş bir görüntü için bir veri URL'si istediğiniz her yerde kullanılabilir.


lütfen bana bu örnek hakkında daha fazla ayrıntı verebilir misiniz, işlevi nasıl çağırırım ve sonucu nasıl döndürür?
visulo

İşte bir örnek: danielsadventure.info/Html/scaleimage.html Nasıl çalıştığını görmek için sayfanın kaynağını okuduğunuzdan emin olun.
Vivian River

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/… @DanielAllenLangdon tarafından önerilen bağlantıyı okumak isteyen diğer kişiler için
Cedric Arnould

Bir uyarı olarak, image.width / height yüklenmediği için bazen 0 değerini döndürür. Doğru görüntüyü ve yüksekliği elde etmek için bunu zaman uyumsuz bir işleve dönüştürmeniz ve image.onload'ı dinlemeniz gerekebilir.
Matt Pope


4

downscaleImage()Yukarıda @ daniel-allen-langdon tarafından yayınlanan işlevle ilgili bir sorun yaşadım, çünkü görüntü yükü eşzamansız olduğundan image.widthve image.heightözelliklerinin hemen kullanılamaması .

Lütfen bunu dikkate alan, asyncişlevleri kullanan ve görüntüyü yalnızca genişliği değil en uzun boyuta göre yeniden boyutlandıran güncellenmiş TypeScript örneğine bakın.

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

}

Ben açıklamasını eklersiniz quality, resolutionve imageType(bu biçimi)
Barabas

3

Düzenleme: Bu cevapla ilgili Bay Me yorumuna göre, sıkıştırmanın artık JPG / WebP formatları için mevcut olduğu görülüyor (bkz. Https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL ) .

Bildiğim kadarıyla görüntüleri tuval kullanarak sıkıştıramazsınız, bunun yerine yeniden boyutlandırabilirsiniz. Canvas.toDataURL kullanmak, kullanılacak sıkıştırma oranını seçmenize izin vermez. Tam olarak istediğinizi yapan canimage'e göz atabilirsiniz: https://github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

Aslında, boyutunu küçültmek için genellikle resmi yeniden boyutlandırmak yeterlidir, ancak daha ileri gitmek isterseniz, görüntü verilerini içeren bir arabellek almak için yeni tanıtılan file.readAsArrayBuffer yöntemini kullanmanız gerekir.

Ardından, içeriğini görüntü formatı spesifikasyonuna ( http://en.wikipedia.org/wiki/JPEG veya http://en.wikipedia.org/wiki/Portable_Network_Graphics ) göre okumak için bir DataView kullanın .

Görüntü veri sıkıştırmasıyla uğraşmak zor olacak, ancak daha kötü bir deneme. Öte yandan, görüntünüzü küçültmek için PNG başlıklarını veya JPEG exif verilerini silmeyi deneyebilirsiniz, bunu yapmak daha kolay olmalıdır.

Başka bir arabellekte başka bir DataWiew oluşturmanız ve onu filtrelenmiş görüntü içeriğiyle doldurmanız gerekir. Ardından, window.btoa'yı kullanarak görüntü içeriğinizi DataURI'ye kodlamanız gerekir.

Benzer bir şey uygularsanız bana bildirin, kodu gözden geçirmek ilginç olacaktır.


Bunu gönderdiğinizden beri bir şeyler değişmiş olabilir, ancak bahsettiğiniz canvas.toDataURL işlevinin ikinci argümanı , uygulamak istediğiniz sıkıştırma miktarıdır.
wp-overwatch.com

2

Kabul edilen cevaba kıyasla daha basit bir çözüm olduğunu görüyorum.

  • HTML5 FileReader API kullanarak dosyaları okuyun. .readAsArrayBuffer
  • Dosya verileriyle bir Blob oluşturun ve URL'sini alın window.URL.createObjectURL(blob)
  • Yeni Görüntü öğesi oluşturun ve src dosyasını blob url dosyasına ayarlayın
  • Resmi tuvale gönderin. Tuval boyutu istenen çıktı boyutuna ayarlanır
  • Kanvas dan ölçekli aşağı veri geri alın yoluyla canvas.toDataURL("image/jpeg",0.7)(kendi çıktı biçimini ve kalitesini ayarlayın)
  • Yeni gizli girişleri orijinal forma ekleyin ve dataURI görüntülerini temelde normal metin olarak aktarın
  • Arka uçta dataURI'yi okuyun, Base64'ten kodunu çözün ve kaydedin

Sorunuza göre:

Yüklemeden önce bir resmi (çoğunlukla jpeg, png ve gif) doğrudan tarayıcı tarafında sıkıştırmanın bir yolu var mı?

Çözümüm:

  1. Doğrudan ile dosyayla bir blob oluşturun URL.createObjectURL(inputFileElement.files[0]).

  2. Kabul edilen cevapla aynı.

  3. Kabul edilen cevapla aynı. Bundan bahsetmeye değer, tuval boyutu gereklidir ve kullanmak img.widthve img.heightayarlamak canvas.widthve canvas.height. Değil img.clientWidth.

  4. Şununla küçültülmüş görüntüyü alın canvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5). Ayarın 'image/jpg'hiçbir etkisi yoktur. image/pngayrıca desteklenmektedir. Yeni bir yap Fileiçindeki nesneyi callbackfunction vücuda sahip let compressedImageBlob = new File([blob]).

  5. Yeni gizli girişler ekleyin veya javascript ile gönderin . Sunucunun hiçbir şeyin kodunu çözmesi gerekmez.

Tüm bilgiler için https://javascript.info/binary adresini kontrol edin . Bu bölümü okuduktan sonra çözümü buldum.


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>

Bu kod, diğer yanıtlardan çok daha az korkutucu görünüyor ..

Güncelleme:

Kişi her şeyi içine koymalı img.onload. Aksi takdirde canvas, zaman canvasatandığı için görüntünün genişliğini ve yüksekliğini doğru olarak alamayacaktır .

    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 .pngimage/jpegbağımsız değişken seti ile dosya sıkıştırma testi .

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


0

işlevi şu şekilde geliştirdim:

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

kullanımı:

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

zevk almak ;)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.