JavaScript を PNG に圧縮する
JavaScript を PNG に圧縮するツールを作りました。JS_Packer
demoscene は最近 WebGL を使ったものも多くなってきています。
demoecene は基本的に
- ローカルにファイルとして存在しているものを使う
- そのファイル容量は 1 バイトでも少ないほうがいい (容量制限がある分野がある)
という文化です。そして JS ファイルを圧縮する手法の一つに、JS を PNG 画像にして、それをデコードする、という手法が存在します。
JS の性質
JS のコードは基本的にアスキー文字の集まりです。アスキーコードは、小文字/大文字のアルファベット、数字、スペースといった 128 種類しか存在しません。
PNG8 の性質
8 ビット PNG は 256 種類の色をパレットに持っています。
PNG は可逆圧縮(ロスレス)形式の画像です。圧縮しても失われるデータはありません。
コードを色に変換しで画像をつくる
JS のコードは 128 種類の文字で構成されており、PNG8 は 256 色で構成されています。文字 (アスキーコード) を色に変換しても余裕があります。
コードから画像を作るには、JS の 1 文字目の文字を画像の 1 ピクセル目の色、 2 文字目を 2 ピクセル目…とするだけです。
2D canvas の imagedata
の機能を使えば、「n ピクセル目の色」を作ることができます。例えば次のようなコードでそれができます。
1 | var JScode = 'alert( 1 );' |
コードから作られた画像
上記の仕組みで、コードを色にすると例えば次のような画像を作ることができます。
jQuery
three.js
画像の大きさは、元 JS のコード量に応じて大きくなります。圧縮が聞いているのでファイル容量は生の JS ファイルよりも小さくなります。
画像をデコードする
作った画像をデコードするのも簡単です。「色 = 文字」なので、色コードをアスキーコードにも戻すだけです。
以下のは、HTML Image を渡すと色からコードに戻す例です。
1 | var decode = function ( $img ) { |
自己解凍の仕組み
PNG ファイル自身に自己解凍の仕組みをつけることができます。
PNG ファイルの任意チャンクとして
1 | '<canvas id=c><img onload=for(w=c.width=' + width + ',h=c.height=' + height + ',a=c.getContext(\'2d\'),a.drawImage(this,p=0,0),e=\'\',d=a.getImageData(0,0,w,h).data;t=d[p+=4];)e+=String.fromCharCode(t);(1,eval)(e) src=#>' |
を PNG のバイナリーデータ内に差し込みます。そして、その PNG 画像をHTMLとしてブラウザーで開けば、
- まず HTML として解析される
- img の src に自分を指定し、画像として自分を再度読み込む
- img の onload 属性値が実行される
- canvas に自分を描画して、全てのピクセルをアスキー文字として解凍する
- eval でそのアスキー文字の集まりを JS として実行する
という流れで自己解凍、実行ができます。
できるだけファイルの先頭に近い部分に、開始タグだけの canvas を配置すれば、バイナリーデータがテキストとして表示されてしまうことを防ぐことができます。
- PNG ファイルの先頭 8 バイトはシグネチャーとして固定
- PNG ファイルの一番最初のチャンク IHDR は、ヘッダーチャンクとして固定、長さは 25 バイト固定
なので、IHDR の次のチャンクになるように、33 バイト目にこの任意チャンクを差し込みます。
自己解凍後は、問題なく JS が動きますので、removeChild
や createElement
で自由に DOM を操作できます。
まとめ
大体の場合、gzip のほうが圧縮率が高くなります。demoscene のような特別な縛りがなければ gzip を使ったほうがいいでしょう。