まだ重たいCMSをお使いですか?
毎秒1000リクエスト を捌く超高速CMS「adiary

2019/07/05(金)JavaScriptから .png 画像を加工してCSSに適用する

png画像をJavaScript上で加工して、CSSに適用する方法。

背景

adiaryでは、UIアイコン(このサイトだとページ送りやタグ一覧に出ている▼等)を色指定に基づいて動的にロードしています。

仕組みとしては、予め128種類の色の異なる画像アイコンを用意して、指定色にもっとも近い色をロードするという感じになっていますが、いくつか問題があります。

  • 画像ファイルを増やすには限界があり近似色になってしまう。
  • 1KBにも満たないような小さなファイルをわざわざ別ファイルとしてロードさせるのは嫌。*1

色が固定で良いなら、base64で埋め込むだけの話なのですが……。

.ui-icon {
	background-image:	url('data:image/png;base64,XXXX----XXX=');
}

adiaryはテーマの色をユーザーが自由に変更できるという素敵な機能が付いておりまして、そういうわけにも行きません……。

*1 : Webの表示速度を上げるためにも、ロードファイル数は極力少ないほうが良いわけです。

正攻法

canvasタグを使って画像を読み込み加工してから、画像として取り出し、cssに適用します。

var canvas  = document.createElement('canvas');
var context = canvas.getContext('2d');

var img = new Image();
img.src = 'data:image/png;base64,XXXX----XXX=';

img.onload = function(){
	context.drawImage(img, 0, 0);

	// 画像の加工

	var data = canvas.toDataURL("image/png");
	var dom = document.getElementsByClassName("ui-icon");
	dom.style.backgroundImage = 'url("' + data + '")';
}

※動作テストはしていません。あしからず。

ファイルを直接加工

今回の目的は色を変えたいだけなので、png画像データを直接書き換えれば良いのでは? というアイデアが生まれました。都合が良いことに、PNG画像のパレットデータは無圧縮で記録されています。*2

  1. base64化したPNG画像を埋め込む。
  2. base64を atob() で戻してバイナリ列を得る。
  3. パレットデータを書き換える。
  4. 書き換えた画像データを btoa() でbase64に戻し、スタイルに適用する。
function png(R,G,B) {;
	let png = atob( 'XXXX----XXX=')

	//色の変更
	const color = String.fromCharCode(R, G, B);

	// palette 0番目, 画像ファイルから予め設定しておく
	const PALATTE_OFFSET = 0x29;

	// パレットの書き換え
	png = png.substr(0, PALATTE_OFFSET) + color + png.substr(PALATTE_OFFSET+3);

	// CRC32
	const GEN    = 0xEDB88320;
	const offset = 0x25;	// PLETチャンクの位置(予め設定)
	const length = 10;	// PLETチャンク長

	const data = png.substr(offset, length);
	let crc = calc_png_crc32(data);		// CRC32関数(省略)

	crc = String.fromCharCode(
		(crc>>>24) & 0xff,
		(crc>>>16) & 0xff,
		(crc>>> 8) & 0xff,
		 crc       & 0xff
	);

	// PLTEチャンクのCRCの書き換え
	const p = offset + length;
	png = png.substr(0, p) + crc + png.substr(p+4);

	// base64 画像化
	const img = 'url("data:image/png;base64,' + btoa(png) + '")';

	// スタイル適用
	const dom = document.getElementsByClassName("ui-icon");
	dom.style.backgroundImage = 'url("' + data + '")';

※CRC32の計算関数は省略しました。詳細は adiary のソースを見てください。

*2 : GIFはパレット領域も圧縮されているようです。

まとめ

JavaScriptでCRC32を実装しているときに、整数に型がないので(符号無視)右シフトができずハマっていました……。「>>>」演算子なんて聞いてない(苦笑)