🤖

JS 画像のBASE64変換と変換画像の品質管理について

に公開

目的

画像のBASE64の変換とサイズの考え方を理解する。
最適な画像圧縮をするための考え方を理解する。

背景

electronで、リッチテキストエディタを製作している。このリッチテキストエディタは、画像の設定も可能です。実際に設定してみたところ、10枚で20MBにもなることが分かった。

JSONですべてを管理するリッチテキストエディタにおいて、JSONの中に20MBの画像のデータが入っていることは、読み込みの遅延につながり問題である。

そのため、画像を圧縮することにした。

圧縮先の画像拡張子の選定

画像の種類は、jpeg,png,webp等あるが、今回は、webpにした。
webpが一番圧縮と品質のバランスがいいためだ。

品質は80とする。

品質は調べたところによると、85が最適だそうだ。

しかし、実際に変換をしてみると、画像の解像度のほうが重要で品質を下げてでも解像度を上げたほうがいいという結論になった。

これは、実際に試しながら調整してください。

私のプロジェクトでは、画像は、10枚は表示したかったので、
仮に上限を1MBに定めると、一枚当たり0.1MBが目標値ということになります。

変換方法

canvasに描画して、それをもとに変換する。

const canvas = document.createElement('canvas');
let width = image.width;
let height = image.height;


canvas.width = width;
canvas.height = height;

const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0, width, height);
let dataUrl_base64_01mb = canvas.toDataURL('image/webp', quality);

重大な落とし穴。

画像の圧縮をしてみると、画像によってよく圧縮されるものとそうでないものがあることに気が付く。

シンプルなイラストなどは、小さく圧縮される。

その一方で複雑でグラデーションのあるような画像は、そこまで圧縮されない。(といっても9割は減るので安心してください)

このシンプルな画像は、文字等が書かれているとノイズが目立ちやすいという特徴があり、
慎重に変換の基準を定める必要がある。

品質のパラメータと、横縦の解像度を決めうちで機械的に変換を行うと、痛い目に合います。

試行錯誤の結果

できるだけ高い解像度で画像を保存することが品質を保つうえで重要になります。

結果、
最初に、元の画像解像度でwebp変換し、その画像サイズを取得する。
そのサイズが、0.1MB以下であればOK、
そうでなければ再生成を行うというコードを作成した。

base64の場合、 変換後の文字列の文字数 / 1000000でMBが出るので、
豆知識として覚えておくといいと思います。

再生成の際には、

const assyukuritu = Math.round(Math.sqrt(0.07/(dataUrl_base64_01mb.length / 1000000)) * 40  ) /40;

を使う。
0.1MBではなく、0.07MBに調整をしていますが、気にしないでください。

Math.sqrt(0.07/(dataUrl_base64_01mb.length / 1000000))

が重要箇所で、

ここでは、どれだけ画像解像度を収縮させればいいかを計算しています。
0.07MBが目標で右が実際のMBです。

たとえば、現在の画像容量が0.14MBの場合、

0.07 / 0.14で 0.5となります。

これで、画像を半分にしたらいいのかなと思いきやそうではありません。

縦、横ともに、0.5を掛け合わせると、最終画像は、0.25倍になってしまいます。

圧縮しすぎですね。

二回かけると、0.5になる数が欲しいです。

それは、sprt()関数で取ることができ、結果、上記のようなコードになります。

Math.round()は四捨五入の関数ですが、
これは、きりの良い数字のほうがいいことがあるかもしれないという意味でつけたもので、実際に意味があるかはわかりません。

40をかけて 整数変換後 40で割ると、
2.5%刻みになります。

説明は以上になります。

以下、実際のコードです。


// 画像をリサイズし、Base64に変換する関数
function resizeImage(file, quality = 0.8) {
    return new Promise((resolve) => {
        const reader = new FileReader();
        reader.onload = (readerEvent) => {
            const image = new Image();
            image.onload = (imageEvent) => {
                const canvas = document.createElement('canvas');
                let width = image.width;
                let height = image.height;


                canvas.width = width;
                canvas.height = height;

                const ctx = canvas.getContext('2d');
                ctx.drawImage(image, 0, 0, width, height);
                let dataUrl_base64_01mb = canvas.toDataURL('image/webp', quality);
                
                
                if(dataUrl_base64_01mb.length / 1000000 > 0.07){
                    const assyukuritu = Math.round(Math.sqrt(0.07/(dataUrl_base64_01mb.length / 1000000)) * 40  ) /40;
                    console.log("圧縮率 " + assyukuritu+ "でデータを圧縮します");
                    canvas.height*=assyukuritu;
                    canvas.width*=assyukuritu;
                    const ctx2 = canvas.getContext('2d');
                    ctx2.drawImage(image, 0, 0, width*assyukuritu, height*assyukuritu);
                    dataUrl_base64_01mb = canvas.toDataURL('image/webp', quality);
                }
                
                    
            
               canvas.remove();
                resolve(dataUrl_base64_01mb);
            };
            image.src = readerEvent.target.result;
        };
        reader.onerror = (error) => {
            console.error('File reading error for resize:', error);
            resolve(null); // エラー時はnullを返す
        };
        reader.readAsDataURL(file);
    });
}

Discussion