ブラウザでJavascriptを介して画像を圧縮する方法は?


91

TL; DR;

アップロードする前に、画像(主にjpeg、png、gif)をブラウザ側で直接圧縮する方法はありますか?JavaScriptでこれができると確信していますが、それを実現する方法が見つかりません。


これが私が実装したい完全なシナリオです:

  • ユーザーが私のウェブサイトにアクセスし、input type="file"要素を介して画像を選択し、
  • この画像はJavaScriptを介して取得されます。正しいファイル形式、最大ファイルサイズなど、いくつかの検証を行います。
  • すべて問題がなければ、画像のプレビューがページに表示されます。
  • ユーザーは、画像を90°/ -90°回転したり、事前定義された比率に従ってトリミングしたりするなどの基本的な操作を実行できます。または、ユーザーは別の画像をアップロードして手順1に戻ることができます。
  • ユーザーが満足すると、編集された画像は圧縮され、ローカルに「保存」されます(ファイルには保存されませんが、ブラウザのメモリ/ページに保存されます)。
  • ユーザーは、名前、年齢などのデータをフォームに入力します。
  • ユーザーが[完了]ボタンをクリックすると、データと圧縮画像を含むフォームがサーバーに送信されます(AJAXなし)。

最後のステップまでの全プロセスはクライアント側で実行する必要があり、最新のChromeとFirefox、Safari5以降およびIE8以降と互換性がある必要があります。可能であれば、JavaScriptのみを使用する必要があります(ただし、これは不可能であると確信しています)。

今は何もコーディングしていませんが、すでに考えています。ファイルAPIを介してローカルでファイルを読み取ることができます。画像のプレビューと編集はCanvas要素を使用して行うことができますが、画像圧縮部分を行う方法が見つかりません

html5please.comcaniuse.comによると、これらのブラウザのサポートは非​​常に困難です(IEのおかげで)が、FlashCanvasFileReaderなどのポリフィルを使用して行うことができます。

実際、目標はファイルサイズを小さくすることなので、解決策として画像圧縮を考えています。しかし、アップロードされた画像が毎回同じ場所で私のWebサイトに表示されることはわかっており、この表示領域のサイズ(200x400など)も知っています。そのため、これらのサイズに合わせて画像のサイズを変更し、ファイルサイズを小さくすることができました。この手法の圧縮率はどうなるかわかりません。

どう思いますか ?何かアドバイスはありますか?JavaScriptでブラウザ側の画像を圧縮する方法を知っていますか?返信ありがとうございます。

回答:


164

要するに:

  • .readAsArrayBufferを使用してHTML5FileReaderAPIを使用してファイルを読み取ります
  • ファイルデータを使用してBLOBを作成し、window.URL.createObjectURL(blob)使用してそのURLを取得します
  • 新しいImage要素を作成し、そのsrcをファイルbloburlに設定します
  • 画像をキャンバスに送信します。キャンバスサイズは希望の出力サイズに設定されます
  • 縮小されたデータをcanvas.toDataURL( "image / jpeg"、0.7)を介してキャンバスから取得します(独自の出力形式と品質を設定します)
  • 新しい非表示の入力を元のフォームに添付し、dataURI画像を基本的に通常のテキストとして転送します
  • バックエンドで、dataURIを読み取り、Base64からデコードして、保存します

ソース:コード


1
どうもありがとう !これが私が探していたものです。この手法で圧縮率がどれほど優れているか知っていますか?
pomeh 2013

2
@NicholasKyriakidescanvas.toDataURL("image/jpeg",0.7)効果的に圧縮し、JPEGを品質70(デフォルトの品質100ではなく)で保存することを確認できます。
user1111929 2015年

4
@ニコラス・キリアキデス、これは良い区別ではありません。ほとんどのコーデックはロスレスではないため、「ダウンスケーリング」定義に適合します(つまり、100に戻すことはできません)。
ビリーボボンネット2015年

5
ダウンスケーリングとは、高さと幅の点で小さいサイズの画像を作成することです。これは本当に圧縮です。非可逆圧縮ですが、確かに圧縮です。ピクセルをダウンスケーリングするのではなく、一部のピクセルを同じ色に微調整して、圧縮がより少ないビットでそれらの色にヒットできるようにします。JPEGにはとにかくピクセルの圧縮が組み込まれていますが、非可逆モードでは、いくつかの色が同じ色と呼ばれる可能性があると言われています。それはまだ圧縮です。グラフィックスに関するダウンスケーリングは、通常、実際のサイズの変更を指します。
2016年

3
私はこれを言いたいだけです:ファイルはURL.createObjectUrl()blobに変えずに直接行くことができます。ファイルはblobとしてカウントされます。
hellol11 2016

20

私は他の答えから2つのことが欠けているのを見ます:

  • canvas.toBlob(利用可能な場合)は、よりもパフォーマンスが高くcanvas.toDataURL、非同期でもあります。
  • ファイル->画像->キャンバス->ファイル変換はEXIFデータを失います。特に、最近の携帯電話/タブレットで一般的に設定されている画像の回転に関するデータ。

次のスクリプトは両方の点を扱います。

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

使用例:

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は私のためにトリックを行い、ファイルを作成し、サーバー上の$ _FILES配列を受け取りました。ありがとうございました!
リカルドルイスロメロ

素晴らしく見える!しかし、これはすべてのブラウザ、Web、モバイルで機能しますか?(IEを無視しましょう)
GarvitJain19年

14

@PsychoWoodsの答えは良いです。私自身の解決策を提供したいと思います。このJavascript関数は、画像データのURLと幅を取得し、それを新しい幅にスケーリングして、新しいデータURLを返します。

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

このコードは、データURLがあり、縮小された画像のデータURLが必要な場所であればどこでも使用できます。


この例、関数の呼び出し方法、結果の返送方法について詳しく教えてください。
visulo 2017年

次に例を示します。danielsadventure.info / Html / scaleimage.htmlページのソースを読んで、どのように機能するかを確認してください。
ビビアン川

1
web.archive.org/web/20171226190510/danielsadventure.info/Html/... リンクは@DanielAllenLangdonによって提案読みたい、他の人のために
セドリックArnould

ヘッドアップとして、image.width / heightがロードされていないため、0を返す場合があります。これを非同期関数に変換し、image.onloadをリッスンして、高さの正しい画像を取得する必要がある場合があります。
マット・ポープ


4

downscaleImage()@ daniel-allen-langdonによって上記に投稿された関数で、画像の読み込みが非同期であるため、プロパティimage.widthimage.heightプロパティがすぐに使用できないという問題がありました。

これを考慮し、async関数を使用し、幅だけでなく最長の寸法に基づいて画像のサイズを変更する、以下の更新されたTypeScriptの例を参照してください。

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

}

私はの説明追加しqualityresolutionそしてimageType(これのフォーマット)
Barabas

3

編集:この回答に対するMr Meのコメントによると、圧縮がJPG / WebP形式で利用できるようになっているようです(https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURLを参照)。 。

私の知る限り、キャンバスを使用して画像を圧縮することはできません。代わりに、サイズを変更することができます。canvas.toDataURLを使用しても、使用する圧縮率を選択することはできません。あなたはまさにあなたが望むことをするcanimageを見ることができます:https//github.com/nfroidure/CanImage/blob/master/chrome/canimage/content/canimage.js

実際、多くの場合、画像のサイズを変更してサイズを小さくするだけで十分ですが、さらに先に進む場合は、新しく導入されたメソッドfile.readAsArrayBufferを使用して、画像データを含むバッファーを取得する必要があります。

次に、DataViewを使用して、画像形式の仕様(http://en.wikipedia.org/wiki/JPEGまたはhttp://en.wikipedia.org/wiki/Portable_Network_Graphics)に従ってコンテンツを読み取ります。

画像データの圧縮に対処するのは難しいでしょうが、それはもっと悪い試みです。一方、PNGヘッダーまたはJPEG exifデータを削除して画像を小さくすることもできますが、そうする方が簡単です。

別のバッファに別のDataWiewを作成し、フィルタリングされた画像コンテンツで埋める必要があります。次に、window.btoaを使用して、画像コンテンツをDataURIにエンコードする必要があります。

あなたが似たようなものを実装するなら、私に知らせてください、コードを通過するのは面白いでしょう。


これを投稿してから何かが変わった可能性がありますが、あなたが言及したそのcanvas.toDataURL関数の2番目の引数は、適用する圧縮の量です。
wp-overwatch.com 2018年

2

受け入れられた答えと比較して、より簡単な解決策があることがわかりました。

  • HTML5 FileReaderAPIを使用してファイルを読み取ります。 .readAsArrayBuffer
  • ファイルデータを使用してBLOBを作成し、次のURLを取得します。 window.URL.createObjectURL(blob)
  • 新しいImage要素を作成し、そのsrcをファイルbloburlに設定します
  • 画像をキャンバスに送信します。キャンバスサイズは希望の出力サイズに設定されます
  • キャンバスから縮小したデータを取り戻す経由してcanvas.toDataURL("image/jpeg",0.7)(独自の出力形式と品質を設定)
  • 新しい非表示の入力を元のフォームに添付し、dataURI画像を基本的に通常のテキストとして転送します
  • バックエンドで、dataURIを読み取り、Base64からデコードして、保存します

あなたの質問によると:

アップロードする前に、画像(主にjpeg、png、gif)をブラウザ側で直接圧縮する方法はありますか?

私の解決策:

  1. を使用して、ファイルを使用してblobを直接作成しますURL.createObjectURL(inputFileElement.files[0])

  2. 受け入れられた答えと同じ。

  3. 受け入れられた答えと同じ。それを言及する価値が、キャンバスのサイズが必要と使用することであるimg.widthimg.heightセットにcanvas.widthしてcanvas.height。ありませんimg.clientWidth

  4. でスケールダウン画像を取得しcanvas.toBlob(callbackfunction(blob){}, 'image/jpeg', 0.5)ます。設定し'image/jpg'ても効果はありません。image/pngもサポートされています。を使用してFilecallbackfunction ボディ内に新しいオブジェクトを作成しlet compressedImageBlob = new File([blob])ます。

  5. 新しい非表示の入力を追加するか、JavaScriptを介して送信します。サーバーは何もデコードする必要はありません。

すべての情報については、https://javascript.info/binaryを確認してください。この章を読んだ後、私は解決策を思いついた。


コード:

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

このコードは、他の回答よりもはるかに怖くないように見えます。

更新:

すべてを中に入れる必要がありますimg.onload。そうしcanvasないと、時間canvasが割り当てられているため、画像の幅と高さを正しく取得できません。

    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/jpeg引数を設定したファイル圧縮テスト。

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


0

私はこれになるように機能を改善しました:

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

それの使用:

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

楽しい ;)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.