javascriptでBase64

# javascriptでBase64

Data URI Schemeなどで任意のデータのBase64文字列が必要な場合がある。 大体のブラウザでwindow.btoa()及びwindow.atob()が存在するが これに渡せる文字列には制限があり、文字コードが0xff以下でないとエラーになる。

Base64文字列を得る方法として以下を考えた

1. btoaに渡す文字列を工夫する
2. 自前で実装する
3. FileReader.prototype.readAsDataURLを利用する

## btoaに渡す文字列を工夫する

btoa()に直接バイナリデータ文字列(8bit)を渡せば良い。 バイナリデータ文字列ならば文字コードが全て0xff以下であるので正常に変換が可能。

```js
var binstr = String.fromCharCode(227, 129, 130); //=> "\xe3\x81\x82" ←"あ"のUTF-8表現
btoa(binstr); //=> "44GC"
```

エンコードしたいデータが文字データであり、かつ文字コードがUTF-8で良いのならば以下のようにする

```js
var Base64 = {
	encode: function(str) {
		return btoa(unescape(encodeURIComponent(str)));
	},
	decode: function(str) {
		return decodeURIComponent(escape(atob(str)));
	}
};

Base64.encode("Base64エンコード"); //=> "QmFzZTY044Ko44Oz44Kz44O844OJ"
Base64.decode("QmFzZTY044OH44Kz44O844OJ"); //=> "Base64デコード"
```

`encodeURIComponent()`が文字列をUTF-8化してURLエスケープするので、 それをアンエスケープしてUTF-8のバイナリデータ文字列を得る。 文字データに限られるが非常に短く実装できる。

注意すべきなのは、、バイナリデータ文字列を上記の関数に渡すと 文字列に再度UTF-8変換がかかるので、おかしなBase64になってしまう。

```js
var binstr = String.fromCharCode(128, 129, 130); //3byte
Base64.encode(binstr); //=> "woDCgcKC" ←4byteではなく8byteになっている。UTF-8では0x80以上は2byteになる為
btoa(binstr);          //=> "gIGC"     こちらが正しい
```

通常の文字列(printしたものが可読なもの)に対してのみ利用すること

## 自前で実装する

ArrayやArrayBufferから変換するのに一度文字列化するのは無駄な処理になる。 そこでArrayやArrayBufferからBase64文字列へ自前で変換してみる。
### Base64の仕様
Base64は割と簡単な仕組みなので実装は難しくない。 encode時には3byte(8bit * 3)を4byte(6bit * 4)に変換し、A-Za-z0-9+/を割り当てる。 最後4byteに足りない場合には = で埋める。 decode時はその逆である。

### 実装

```js
var Base64a = {
	encode: (function(i, tbl) {
		for(i=0,tbl={64:61,63:47,62:43}; i<62; i++) {tbl[i]=i<26?i+65:(i<52?i+71:i-4);} //A-Za-z0-9+/=
		return function(arr) {
			var len, str, buf;
			if (!arr || !arr.length) {return "";}
			for(i=0,len=arr.length,buf=[],str=""; i<len; i+=3) { //6+2,4+4,2+6
				str += String.fromCharCode(
					tbl[arr[i] >>> 2],
					tbl[(arr[i]&3)<<4 | arr[i+1]>>>4],
					tbl[i+1<len ? (arr[i+1]&15)<<2 | arr[i+2]>>>6 : 64],
					tbl[i+2<len ? (arr[i+2]&63) : 64]
				);
			}
			return str;
		};
	}()),
	decode: (function(i, tbl) {
		for(i=0,tbl={61:64,47:63,43:62}; i<62; i++) {tbl[i<26?i+65:(i<52?i+71:i-4)]=i;} //A-Za-z0-9+/=
		return function(str) {
			var j, len, arr, buf;
			if (!str || !str.length) {return [];}
			for(i=0,len=str.length,arr=[],buf=[]; i<len; i+=4) { //6,2+4,4+2,6
				for(j=0; j<4; j++) {buf[j] = tbl[str.charCodeAt(i+j)||0];}
				arr.push(
					buf[0]<<2|(buf[1]&63)>>>4,
					(buf[1]&15)<<4|(buf[2]&63)>>>2,
					(buf[2]&3)<<6|buf[3]&63
				);
			}
			if (buf[3]===64) {arr.pop();if (buf[2]===64) {arr.pop();}}
			return arr;
		};
	}())
};


Base64a.encode([97, 98, 99, 100]); //=> "YWJjZA=="
Base64a.decode("YWJjZA=="); //=> [97, 98, 99, 100]
```

## ArrayBuffer,Blob(File)などから変換する場合

`Base64a.encode`に渡す配列が Uint8Array ならば良いので

```js
Base64a.encodeArrayBuffer = function(ab) {
	return Base64a.encode(new Uint8Array(ab));
};
Base64a.encodeBlob = function(blob, callback) {
	var r = new FileReader();
	r.onload = function() {callback(Base64a.encodeArrayBuffer(r.result));};
	r.readAsArrayBuffer(blob);
};

var blob = new Blob(["abcd"]);
Base64a.encodeBlob(blob, function(str) {
	console.info(str); //=> "YWJjZA=="
});
```

## FileReader.prototype.readAsDataURLを利用する

`FileReader.prototype.readAsDataURL`ならばbase64を含んだData URIが取得出来る。
`data:mime/type;base64,...Base64String...`という形式なのでカンマ以降を抜き出せばよい。

```js
var blob = new Blob(["abcd"]),
	r = new FileReader();
r.onload = function() {
	console.info(r.result.substr(r.result.indexOf(',')+1));
};
r.readAsDataURL(blob);
```