こんにちは、18 日以降の Graphical Web Advent Calendar が空いているので、場をつなぐ意味も込めて簡単な記事を投稿させていただきます。
先日の記事では PNG の仕様について書きましたが、その知識をさっそく生かす事ができます。

また、この記事では HTMLCanvasElement を省略して Canvas と表記させていただきます。

Canvas#toDataURL()

さて、一般的に Canvas の描画状況を保存しようと思うと、Canvas#toDataURL メソッドを使用すると思います。
ですが、このメソッドで保存された画像がどのようになっているかご存知の方はあまりいないと思います。

まずは、以下のコードで簡単な Canvas 描画を行ってみます。

function draw1(targetId) {
    var target = document.getElementById(targetId);
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d');

    ctx.fillStyle = 'rgb(0, 0, 0)';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    target.src = canvas.toDataURL();
}

これを行うと、以下のような画像が表示されます。

Chrome でこの画像を保存し、IHDR を見てみると以下のようになっています。

Width300
Height150
BitDepth8
ColourTypeTruecolor_with_alpha (6)
CompressionMethodDeflate (0)
FilterMethodBasic (0)
InterlaceMethodNone (0)

使用している色が限られているのに、Truecolour with alpha で保存されています。
Canvas#toDataURL メソッドでは、全ての画像が Truecolour with alpha になってしまうようです。
これは大きな無駄となります。

たとえば WebStorage に保存するときなど、少しでもサイズを小さくするために Indexed Colour やグレイスケールで保存したい場合もあると思います。
ここから、Indexed Colour などで Canvas を保存する方法について説明します。

CanvasTool.PngEncoder

また自作ツールの宣伝で恐縮なんですが、だいぶ前に PNG エンコーダを JavaScript で自作したのでそれを利用します。
まだパフォーマンスの面などで問題はあるんですが、そこそこ使えると思います。

PNG byte-string の作成

このライブラリは以下のように使用します。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 1,
        colourType: CanvasTool.PngEncoder.ColourType.GRAYSCALE
    }
);
var png = encoder.convert();

上記の描画例では、黒で塗りつぶしてるかそうでないかだけなので、ビット深度 1 のグレイスケール画像で保存します。
また、Indexed Colour で保存する場合は以下のようにします。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 1,
        colourType: CanvasTool.PngEncoder.ColourType.INDEXED_COLOR
    }
);
var png = encoder.convert();

(注意: Indexed Colour では使用している色数が 2^bitDeph を超えていたら例外を投げます。色数がパレットの最大数を超えないように使用してください。)

単純に RGBA ではなく RGB で保存したい場合は以下のようにします。

var encoder = new CanvasTool.PngEncoder(
    canvas,
    {
        bitDepth: 8,
        colourType: CanvasTool.PngEncoder.ColourType.TRUECOLOR
    }
);
var png = encoder.convert();

ちなみにこのライブラリでは Truecolour などで bitDepth: 16 も指定できますが、元の色が bitDepth:8 のため意味はないでしょう。

Data URL への変換

変数 png は ByteString なので、これを DataURL で使用するには Base64 形式にエンコードします。(そのままで使える場合もあるのですが、ここでは省略します)
ByteString から Base64 へのエンコードは window.btoa が使える環境ではそれを使うのが最も簡単で高速です。
window.btoa が使えない環境では polyfill を使うのが良いと思います。

window.btoa を使うと、以下のようになります。

var dataUrl = 'data:image/png;base64,' + window.btoa(png);

動作例

実際に前述のコードをそれぞれ動かしてみます。

ビット深度 1 のグレイスケール画像

ビット深度 1 の Indexed Colour 画像

ビット深度 8 の Truecolour 画像

比較

生成した画像をダウンロードしてファイルサイズを比較してみます。( Google Chrome 23 で確認)

Canvas#toDataURL()GrayscaleIndexed ColourTruecolour
1,869106136543

一般的な画像での比較 (16:00追加)

例として使用している Canvas が適当すぎるので、一応きちんとした画像でも比較してみたいと思います。以下の画像を Canvas に drawImage して、それを保存します。

ubunchu01_01_small
  • 架空線 – AERIAL LINE - : 第1話 「うぶんちゅがやって来た!」:
    http://www.aerialline.com/comics/ubunchu/episode01
  • © 瀬尾浩史
  • 元の画像から 1/4 サイズにして 256 色に減色しています
  • いつもお世話になっております

Canvas#toDataURL()

CanvasTool.PngEncoder

Chrome 23 で試したところ、以下のような結果になりました。

Canvas#toDataURL()CanvasTool.PngEncoder
処理時間32 ms1,614 ms
サイズ161,78761,006

処理時間がかかりすぎていますが、CanvasTool.PngEncoder では WebWorkers でも(少し手を加えるだけで)利用可能なので、タイムアウトする可能性がある場合は WebWorkers の利用を考えてみるのも良いかもしれません。

また、今回ためした結果は PC なので Canvas#toDataURL メソッドは割と速く終わっていますが、モバイルなどでは毎フレーム実行するなどの激しい操作はまだ難しいと思います。

おわりに

いかがでしたでしょうか。
Canvas#toDataURL メソッドでは指定できない、PNG の細かい設定を行う事によりかなりの容量の節約ができるようになります。
今回はライブラリを使いましたが、簡単なPNGであれば自作も楽ですのでぜひ挑戦してみてください。

また、自力でエンコードは圧縮処理の都合上どうしても遅くなってしまうので、速さとサイズ、どちらを優先するか見極めて調整することが必要となります。

以上、何かの参考になれば幸いです。