javascriptでBase64
Data URI Schemeなどで任意のデータのBase64文字列が必要な場合がある。 大体のブラウザでwindow.btoa()及びwindow.atob()が存在するが これに渡せる文字列には制限があり、文字コードが0xff以下でないとエラーになる。
Base64文字列を得る方法として以下を考えた
- btoaに渡す文字列を工夫する
- 自前で実装する
- FileReader.prototype.readAsDataURLを利用する
btoaに渡す文字列を工夫する
btoa()に直接バイナリデータ文字列(8bit)を渡せば良い。 バイナリデータ文字列ならば文字コードが全て0xff以下であるので正常に変換が可能。
var binstr = String.fromCharCode(227, 129, 130); //=> "\xe3\x81\x82" ←"あ"のUTF-8表現
btoa(binstr); //=> "44GC"
エンコードしたいデータが文字データであり、かつ文字コードがUTF-8で良いのならば以下のようにする
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になってしまう。
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時はその逆である。
実装
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 ならば良いので
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...
という形式なのでカンマ以降を抜き出せばよい。
var blob = new Blob(["abcd"]),
r = new FileReader();
r.onload = function() {
console.info(r.result.substr(r.result.indexOf(',')+1));
};
r.readAsDataURL(blob);