JavaScript
base64

javascriptでBase64

More than 1 year has passed since last update.

javascriptでBase64

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

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

  1. btoaに渡す文字列を工夫する
  2. 自前で実装する
  3. 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);