2010年08月27日 03:30 [Edit]

Ajax - データ交換 via PNG

Demo

実際に双方向に変換するデモを作ってみた。

復元された文書

ソースはこんな感じ。

/*
 * You need base64.js.
 * cf: http://blog.livedoor.jp/dankogai/archives/51067688.html
*/

img2txt = function(node) {
    var el  = document.createElement("canvas"),
        ctx = el.getContext("2d"),
        w   = el.width  = el.style.width  = node.width,
        h   = el.height = el.style.height = node.height;
    ctx.drawImage(node, 0, 0);
    var d = ctx.getImageData(0, 0, w, h).data,
        a = [];
    for(var i = 0, l = d.length; i < l; i += 4) {
        if (d[0]) a.push(String.fromCharCode(d[i]));
    }
    return Base64.btou(a.join(''));
};

txt2png = function(txt){
    if (!txt || !txt.length) return;
    var bin = Base64.utob(txt),
        el  = document.createElement("canvas"),
        ctx = el.getContext("2d"),
        w   = el.width  = el.style.width  = Math.floor(Math.sqrt(bin.length)),
        h   = el.height = el.style.height = Math.floor(bin.length / w) + 1,
        imd = ctx.createImageData(w, h),
        d   = imd.data;
    for (var i = 0, j = 0, l = bin.length; i < l; i++, j += 4) {
        d[j] = bin.charCodeAt(i);
        d[j+1] = d[j+2] = d[j+3] = 255;
    };
    ctx.putImageData(imd, 0, 0);
    return el.toDataURL('image/png');
};

PNG生成において、RGBAのうちRを除いた残りのチャンネルは255にしている。PNG解読の際、見ているのはRだけなので。R=G=Bにすると見た目グレイスケールの画像が出来るが、ファイルはより大きくなる。重要なのはAが255でないと駄目だということ。これがそれ以外の値だと失敗する。

これを以下のようにして使っている。

/* Textフィールド */
$('#demo0txt')
  .text($('#jssample').text()) /* 適当に長い文字列で初期化 */
  .keyup(function(){
  var text = this.value,
      chars = text.length,
      bytes = Base64.utob(text).length
  $('#demo0 dt').eq(1).text([chars, 'chars,', bytes, 'bytes'].join(' ') )
}).trigger('keyup');

/* Render ボタン */
$('#demo0 input').click(function(){
    var text  = $('#demo0txt').val(),
        durl  = txt2png(text);
    $('#demo0url').text(durl);
    $('#demo0img').attr('src',durl);
    $('#demo0 dt').eq(2)
        .text([durl.length, 'chars,', 
               durl.replace(/.*?,/,'').length * 3/4, 'bytes'].join(' '));
});
/* 画像から文字列を再現 */
$('#demo0img').load(function(){
  $('#demo0dst').text(img2txt(this));
});

実験の結果、以下のことがわかった。

ブラウザは小さなPNGを生成できない

PNGのサイズを見てわかるとおり、Firefoxでは圧縮がいくらかかかるがWebKitではほとんどかからない。canvasではRGBAの4チャンネルPNGしか作れず、この場合に最も適当な1チャンネル=グレイスケールPNGができないというのもこれに拍車をかけている。

たとえばブラウザー側でPNGを生成してXHRで送信するというのはやめた方がいいだろう。素直にサーバー側にdeflateで送るので十分なはずだ。

クロスドメイン通信は出来ない

それでもサーバー側であれば圧縮率を高めたグレイスケールなPNGを用意することは容易である。下はミニファイされていないjQuery 1.4.2のソースをPNG化したものだ。クリックしてみて欲しい。

jquery.png

今度は右をクリックしてみよう。こちらはミニファイされた方のjQueryのソース。

jquery.png

上はテキストがalertされるのに、右はDOM Exceptionになる。

具体的にはctx.drawImage()の元画像がクロスドメイン判定された場合、ctx.getImageData()させないという仕様になっている。

これなら素直にCross-Domain XHRをdeflateサポート下で使った方がよいだろう。Apacheであれば

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/text text/html text/plain text/xml text/css application/x-javascript application/javascript
</IfModule>

とするだけでよい。圧縮と伸長は完全に透過的で、これ以上の指定は一切必要ない。静的ファイルのみならずサーバー側で動的生成されたファイルやXHRでも問題ない。主立ったサイトはほぼすべてそうなっている。ここlivedoor blogも例外ではない。

とはいえ、正直言って「裏口通信」のテクニックとしてはJSONPの発見以来の面白いテクニックだと感銘を受けた。似たようなことを私自身も考えていた事は404 Blog Not Found:UTF-PNG (aka Unicolor)からも伺える。jsは本当に面白い。

Dan the Man with Too Much Data to Exchange


この記事へのトラックバックURL