JavaScript を PNG に圧縮する

Posted :

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
2
3
4
5
6
7
8
var JScode = 'alert( 1 );'
var pixels = JScode.length;
for ( i = 0, l = pixels; i < l; i ++ ) {
imagedata.data[ i * 4 ] = JScode.charCodeAt( i );
}

コードから作られた画像

上記の仕組みで、コードを色にすると例えば次のような画像を作ることができます。

jQuery

three.js

画像の大きさは、元 JS のコード量に応じて大きくなります。圧縮が聞いているのでファイル容量は生の JS ファイルよりも小さくなります。

画像をデコードする

作った画像をデコードするのも簡単です。「色 = 文字」なので、色コードをアスキーコードにも戻すだけです。

以下のは、HTML Image を渡すと色からコードに戻す例です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var decode = function ( $img ) {
var i, l;
var ctx = document.createElement( 'canvas' ).getContext( '2d' );
var data, code = [];
var pixels = $img.width * $img.height;
ctx.drawImage( $img, 0, 0 );
data = ctx.getImageData( 0, 0, $img.width, $img.height ).data;
for ( i = 0; i < pixels; i ++ ) {
code.push( String.fromCharCode( data[ i * 4 ] ) );
}
eval( code.join( '' ) );
}

自己解凍の仕組み

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としてブラウザーで開けば、

  1. まず HTML として解析される
  2. img の src に自分を指定し、画像として自分を再度読み込む
  3. img の onload 属性値が実行される
  4. canvas に自分を描画して、全てのピクセルをアスキー文字として解凍する
  5. eval でそのアスキー文字の集まりを JS として実行する

という流れで自己解凍、実行ができます。

できるだけファイルの先頭に近い部分に、開始タグだけの canvas を配置すれば、バイナリーデータがテキストとして表示されてしまうことを防ぐことができます。

  • PNG ファイルの先頭 8 バイトはシグネチャーとして固定
  • PNG ファイルの一番最初のチャンク IHDR は、ヘッダーチャンクとして固定、長さは 25 バイト固定

なので、IHDR の次のチャンクになるように、33 バイト目にこの任意チャンクを差し込みます。

自己解凍後は、問題なく JS が動きますので、removeChildcreateElement で自由に DOM を操作できます。

まとめ

大体の場合、gzip のほうが圧縮率が高くなります。demoscene のような特別な縛りがなければ gzip を使ったほうがいいでしょう。