サイトマップ

JavaScriptコードスニペット:pngデータを読み込む

URLの形式の1つにdataスキームというのがあるそうです。ちょっとしたコンテンツなら外部ファイルではなく、HTMLなりJavaScriptに埋め込んでしまいたいことがありますよね。そういう問題を解決する方法の1つなのですが、すべてのブラウザで使えるわけではないというところが今ひとつ普及しない原因でしょう。

だったら、古いブラウザでも使えるようにするにはどうしたらよいかと考えると、なんとか埋め込まれたデータを解読して使えるようにすれば良いわけです。で、いろんな方法が考えられているようですが、ここでは非圧縮のpng画像を埋め込んで、それを表示するということを考えます。

非圧縮としたのはデコードを単純化するためで、データ量は増えますが、ここで対象とするのは16×16ドット程度の小さなアイコンなので、気にしないことにします。

次に、pngデータを読み取ったとしてどうやって使うかが問題です。もちろんimgタグは使えません。ここでは、1ドットのdiv要素を並べることによって画像を表示させることにしました。そんなんでちゃんと表示できるの?とやってみるまでは半信半疑でしたが、普通の画像データと区別できない程度にはちゃんと表示されます。

さらに副次的な効果として、「透明pngをサポートしていないブラウザでも透明pngを表示できる」ということがあります。それは結構嬉しい人も多いのではないでしょうか。

結論としては、やってやれないことはないが、実用に使うにはちょっと疑問が残るという感じでしょうか。いずれにしても、最新のdataスキーム対応のブラウザではこんなものを使わなくても表示できるわけですから、あくまでも取り残された古いブラウザを救えるかもしれないという目的からすれば、この程度で妥協すべきかもしれません。

[サンプル実行]←クリックすると下の矩形領域に注意標識のアイコンが表示されます。
var pngdecode = @INCLUDE(pngdecode.js);
var png = new pngdecode(
        'iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAYAAAAmL5yKAAAAAXNSR0IArs4c6QAA' +
        'AAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sH' +
        'GAgdKSBtQOQAAAOZSURBVCgVAY4DcfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAREREA' +
        'ExMTFAQEBcMEBAXAFBQUEhEREQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
        'AAAAAAAAAAAAKSkpAAAAAAAEBAhpIh0B+yAcAfoEBAhlAAAAACoqKgAAAAAAAAAA' +
        'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALCwsACwsOIwkIA9adhgD/mYMA/wgH' +
        'A9MMDA8gDAwMAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAHBwcADw8PAL1' +
        '9vhoPzX9KToxAAA9MwAAPTT9LPT192dWVlQBHR0dAAAAAAAAAAAAAAAAAAAAAAAA' +
        'AAAAAGZmZgAFBQUABgcLOhUSAumljQD/QTgA/0U7AP+jiwD/ExAC5wcICzcFBQYA' +
        'eXl5AAAAAAAAAAAAAAAAAAAAAAAAFRUVABkZGgoBAgWraFgA/+DAAP8iHQD/JSAA' +
        '/+HBAP9jVQD/AQEFpxsbHAkVFRUAAAAAAAAAAAAAAAAAADMzMwAAAAAAAwQJVyMe' +
        'AfbUtQD/9NAA/zQtAP84MAD/9tIA/9GyAP8hHAH1AwQJVAAAAAA3NzcAAAAAAAIA' +
        'AAAA3NzcAA8PERgCAfpwY1X/CSsmAAAHBgAAFxMAABcTAAAGBQAALigAAGFT/woB' +
        'APpwEBASFtjY2AAAAAAAAiIiIgDx8fEA8vP2XzQr/jdhUwAAAP8AAAQEAAAaFgAA' +
        'GhcAAAMEAAAAAQAAY1UAADIq/jnx8vVe8fHxACIiIgAD+Pj4AAUGCSsHBfiNhXH/' +
        'ETgyAAAA/gAAAAIAAPb4AAAiHQAAKyYAAAD+AAANDgAACQYAAbnE/yMEBQy79PPy' +
        '7AIeHh4F9/f4bEg8/iFORAAAAP4AAAAAAAD8+wAA9vYAAPb2AAD9+wAAAAAAAAD+' +
        'AABRRQAARTr+I/f3+WsjIyMEAAUGCk8bFwLtxqsA///fAP//3AD//90A//LRAP8/' +
        'NgD/QzkA//TSAP//3QD//9wA///fAP/DqAD/GRUC7AUGC0wE+/v6fzAp/hL+/AAA' +
        'x8oAAAD/AAAAAQAAAP8AAF5PAAABAQAA0tYAAAAAAAAAAAAAAAEAAAH+AAAvKP4T' +
        'uMT5fgEEBATnCAb+GAYFAAD/AAAAAAAAAAAAAAAAAAAAAwIAAAAAAAD9/gAAAAAA' +
        'AAAAAAAAAAAAAQAAAPr7AAD4+gLkitrNZpu7YIEAAAAASUVORK5CYII='
);
document.getElementById("pngarea").appendChild(png.toDOM());

pngdecode.js
/**
 * pngdecode.js - 非圧縮png画像を読み込む
 *
 * 依存するライブラリ:なし
 * 影響を受けるプロパティ:なし
 */
function(data) {
	var create_stream = function(array) {
		if (!array) array = new Array();
		array.mark = 0;
		array.crc = 0xffffffff;
		array.crctable = [];
		for(var i = 0; i < 256; i++) {  
			var u = i>>>0;  
			for(var j = 0; j < 8; j++) {  
				if(u & 0x1) u = ((u >>> 1) ^ 0xEDB88320)>>>0;
				else        u >>>= 1;  
			}  
			array.crctable[i] = u>>>0;
		}
 
		array.append = function(obj) {
			if (typeof obj == 'object' && typeof obj.length == 'number') {
				for (var i = 0; i < obj.length; i++)
					this.push(obj[i]);
			} else {
				this.push(obj);
			}
		};
		array.read = function(len) {
			if (typeof len != 'number') len = 1;
			if (this.mark >= this.length)
				throw new Error("no more data");
			if (len == 1) {
				this.crc = ((this.crc >>> 8) ^ this.crctable[(this[this.mark] ^ this.crc) & 0xFF])>>>0;
				return this[this.mark++];
			} else {
				result = [];
				for (var i = 0; i < len; i++) result.push(this.read());
				return result;
			}
		};
		array.readShort = function() {
			return ((this.read()<<8) +
				(this.read()));
		};
		array.readInt = function() {
			return ((this.read()<<24) +
				(this.read()<<16) +
				(this.read()<<8) +
				(this.read()))>>>0;
		};
		array.readString = function(len) {
			var str = '';
			for (var i = 0; i < len; i++) {
				str += String.fromCharCode(this.read());
			}
			return str;
		};
		array.readZipShort = function() {
			return ((this.read()) +
				(this.read()<<8));
		};
		array.hasMore = function() {
			if (this.mark >= this.length) return false;
			else return true;
		};
		return array;
	};
 
	function defilter(width, height, bpp, literal) {
		var linesize = width * bpp + 1;
		for (var y = 0; y < height; y++) {
			var offset = y * linesize;
			switch(literal[offset]) { //filter type
			case 0: //None
				break;
			case 1: //Sub
				for (var x = 1; x <= width * bpp; x++) {
					var left = x < bpp + 1 ? 0 : literal[offset + x - bpp];
					literal[offset + x] = (literal[offset + x] + left) & 0xff;
				}
				break;
			case 2: //Up
				for (var x = 1; x <= width * bpp; x++) {
					var above = y <= 0 ? 0 : literal[offset - linesize + x];
					literal[offset + x] = (literal[offset + x] + above) & 0xff;
				}
				break;
			case 3: //Average
				for (var x = 1; x <= width * bpp; x++) {
					var left = x < bpp + 1 ? 0 : literal[offset + x - bpp];
					var above = y <= 0 ? 0 : literal[offset - linesize + x];
					literal[offset + x] = (literal[offset + x] + Math.floor((left + above)/2)) & 0xff;
				}
				break;
			case 4:
				for (var x = 1; x <= width * bpp; x++) {
					var left = x < bpp + 1 ? 0 : literal[offset + x - bpp];
					var above = y <= 0 ? 0 : literal[offset - linesize + x];
					var upleft = y <= 0 ? 0 : (x < bpp + 1 ? 0 : literal[offset - linesize + x - bpp]);
					literal[offset + x] = (literal[offset + x] + (function(a, b, c) {
						var p = (a + b - c);
						var pa = Math.abs(p - a);
						var pb = Math.abs(p - b);
						var pc = Math.abs(p - c);
						if (pa <= pb && pa <= pc) return a;
						else if (pb <= pc) return b;
						else return c;
					})(left, above, upleft)) & 0xff;
				}
				break;
			}
		}
	};
 
	function rgba(width, height, bpp, color, literal) {
		var bytes = [];
		var linesize = width * bpp + 1;
		for (var y = 0; y < height; y++) {
			var offset = y * linesize;
			for (var x = 1; x <= width*bpp; x += bpp) {
				var rgba = 0;
				switch(color) {
				case 0:
					rgba = 0xff000000 +
						(literal[offset + x]<<16) +
						(literal[offset + x]<<8) +
						(literal[offset + x]);
					break;
				case 2:
					rgba = 0xff000000 +
						(literal[offset + x]<<16) +
						(literal[offset + x + 1]<<8) +
						(literal[offset + x + 2]);
					break;
				case 3:
					rgb = 0xff000000 + literal[offset + x]; //PLTE
					break;
				case 4:
					rgba = (literal[offset + x + 1]<<24) +
						(literal[offset + x]<<16) +
						(literal[offset + x]<<8) +
						literal[offset + x];
					break;
				case 6:
					rgba = (literal[offset + x + 3]<<24) +
						(literal[offset + x]<<16) +
						(literal[offset + x + 1]<<8) +
						literal[offset + x + 2];
					break;
				}
				bytes.push(rgba>>>0);
			}
		}
		return bytes;
	};
 
	function decodeB64(data) {
		var stream = create_stream();
		var seed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
		data.replace(/\s+/, '')
			.replace(/[a-zA-Z0-9\+\/]{1,4}/g, function(match) {
			var c = 0;
			for (var i = 0; i < match.length; i++) {
				c = (c << 6) + (seed.indexOf(match.substr(i,1)) & 0x3f);
			}
			switch(match.length) {
			case 4: stream.push((c>>16)&0xff);
				stream.push((c>>8)&0xff);
				stream.push(c&0xff);
				break;
			case 3:
				stream.push((c>>8)&0xff);
				stream.push(c&0xff);
				break;
			case 2:
				stream.push((c>>4)&0xff);
			}
		});
		return stream;
	};
 
	/**
	 * Load png image data by BASE64 encoded string or array.
	 */
	this.load = function(data) {
		var pngstream;
		if (typeof data == 'string') pngstream = decodeB64(data);
		else pngstream = create_stream(data);
 
		//read header
		var header = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
		for (var i = 0; i < header.length; i++) {
			var b = pngstream.read();
			if (b != header[i])
				throw new Error("bad header");
		}
 
		//read CHUNK
		var chunks = {};
		var width = 0, height = 0, color = 0, bpp = 0;
 
		while (pngstream.hasMore()) {
			var chunk = {};
			chunk.size = pngstream.readInt();
			pngstream.crc = 0xffffffff; //reset crc
			chunk.type = pngstream.readString(4);
			chunk.data = {};
			var stream = create_stream();
			for (var i = 0; i < chunk.size; i++)
				stream.push(pngstream.read());
			var crc = pngstream.crc;
			chunk.crc  = pngstream.readInt() >>> 0;
			/*
			if (crc != chunk.crc)
				console.log("bad crc:" +
					('00000000' + pngstream.crc.toString(16)).substr(-8,8) +
					"<=>" +
					('00000000' + chunk.crc.toString(16)).substr(-8,8));
			*/
			switch(chunk.type) {
			case 'IHDR':
				chunk.data.width = width = stream.readInt();
				chunk.data.height = height = stream.readInt();
				chunk.data.depth = stream.read();
				chunk.data.color = color = stream.read();
				chunk.data.compress = stream.read();
				chunk.data.filter = stream.read();
				chunk.data.interlace = stream.read();
				chunks[chunk.type] = chunk;
				break;
 
			case 'IDAT':
				if (!chunks['IDAT']) {
					chunk.data = stream;
					chunks['IDAT'] = chunk;
				} else {
					chunks['IDAT'].data.append(stream);
				}
				break;
 
			default:
				chunk.data = stream;
				chunks[chunk.type] = chunk;
			}
			//console.log(chunk);
		}
 
		//read deflated data
		//http://www.aya.or.jp/~sanami/peace/memorial/code21-30.html
		var chunk = chunks['IDAT'];
		var stream = chunk.data;
		var b = stream.read();
		chunk.data.CINFO = (b>>4)&0x0f;
		chunk.data.CM = b&0x0f;
		b = stream.read();
		chunk.data.FLEVEL = (b>>6)&0x03;
		chunk.data.FDICT  = (b>>5)&0x01;
		chunk.data.FCHECK = b&0x1f;
 
		b = stream.read();
		chunk.data.BTYPE = (b>>1)&0x03;
		if (chunk.data.BTYPE != 0)
			throw new Error('Unsupported compression ' + chunk.data.BTYPE + '!');
		chunk.data.BFINAL = b&0x01;
		chunk.data.LEN = stream.readZipShort();
		chunk.data.NLEN = stream.readZipShort();
		switch(color) {
		case 0: bpp = 1; break;
		case 2: bpp = 3; break;
		case 3: bpp = 1; break; //PLTE
		case 4: bpp = 2; break;
		case 6: bpp = 4; break;
		}
 
		chunk.data.LITERAL = stream.read(chunk.data.LEN);
		chunk.data.ADLER32 = stream.readInt();
 
		//defilter
		defilter(width, height, bpp, chunk.data.LITERAL);
 
		//rgba
		chunks['IDAT'].data.IMAGE = rgba(width, height, bpp, color, chunk.data.LITERAL);
 
		return this.img = {
			width  : chunks['IHDR'].data.width,
			height : chunks['IHDR'].data.height,
			data   : chunks['IDAT'].data.IMAGE
		};
	};
 
	/**
	 * Create rendered image DOM document.
	 */
	this.toDOM = function(scale) {
		if (typeof scale != 'number') scale = 1;
		var dot = function(x, y, color) {
			var p = canvas.appendChild(document.createElement('div'));
			p.style.position = 'absolute';
			p.style.overflow = 'hidden';
			p.style.width = scale + 'px';
			p.style.height = scale + 'px';
			p.style.left = (scale * x) + 'px';
			p.style.top = (scale * y) + 'px';
			var hex = ('000000' + (color&0xffffff).toString(16));
			hex = hex.substr(hex.length - 6, 6);
			p.style.backgroundColor = '#' + hex;
			var opacity = (color>>>24)/0xff;
			p.style.opacity = opacity;
			p.style.filter = 'alpha(opacity=' + (opacity*100) + ')';
		}
		var canvas = document.createElement('div');
		canvas.className = canvas.styleClass = 'png-image';
		canvas.style.overflow = 'hidden';
		canvas.style.width = (scale * this.img.width) + 'px';
		canvas.style.height = (scale * this.img.height) + 'px';
		canvas.style.position = 'relative';
		canvas.style.cssFloat = canvas.style.styleFloat = 'left';
		for (var y = 0; y < this.img.height; y++) {
			for (var x = 0; x < this.img.width; x++) {
				dot(x, y, this.img.data[this.img.width * y + x]);
			}
		}
		return canvas;
	};
 
	if (data) this.load(data);
}

関連コンテンツ

参照

 
research/1310375466.txt · 最終更新: 2011/07/26 14:40 by Kazuyuki Matsuda
特に明示されていない限り、本サイトの内容は次のライセンスに従います:Copyright(C) 2011 Shorindo, Inc. All Rights Reserved
Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki