全ての記事

JavaScript String, Binary Data と Base64

2018-02-04 @sunderls

js binary

最近bluetooth APIを読んでいます。そこにたくさんのバイナリーデータ操作があるので、js側でのバイナリー操作についてめもしときます。

atob, btoa, base64

まずwindow.atob()window.btoa()が何をやってるかから見てみましょう。 mdnにてbtoa()の説明からでは、 atobとbtoaはbase64のエンコードとデコードするためのメソッドです。

そもそもbase64って何?

うん、httpのフル名称はhypertext transfer protocolですね。 名前からわかりますが、これはtext-basedのプロトコルです。

じゃなぜ文字列なの?まあ、色々メリットあります。w ここへ)。

文字列なら、ファイルや画像などのデータはどう送信するには、あるルールに従ってデータを文字列に変換する必要です。

コンピューターの世界では0と1しかないので、もし8-bitづつで文字列にすればいいじゃない? そうではない、なぜなら最初にascでは7bitだから、倍数ではなくてよくないです。 あと違うOSでは文字列の処理が違う、勝手に文字列にすればエラーを起こす可能性があり、 どのOSがでも安全に処理できる文字をピックアップする必要があります。

そこでBase64のルールが生まれた。要するに最終的に64個の文字に変換するルールです。ここでルールをざっくり説明します(wikipedia)。

例えば'Man'って文字列

キャラクター M a n
asc 77 97 110
バイナリー 01001101 01100001 01101110

Base64では6-bitづつ区切ってますので、以下の四つ分になります。

バイナリー 010011 010110 000101 101110
base64に T W F u

これで'TWFu'を得た。consoleで確認しましょう。

console.assert(btoa('Man') === 'TWFu')

なので、Base64は3つの文字を4つにしています。もっと長くしていますね。 もしそもそも3の倍数になってない場合は、最後に0を足す。0にマッピングするのは=なので、よくurlやメールの添付ファイルに==を見かけますね。

これでBase64の説明が終わりました。

data url

Data URLもbase64を利用してます。

フォーマットは:

data:[<mediatype>][;base64],<data>

テストしてみましょう、例えば中身は'Man'しかないテキストファイルとします、以下のdata-urlを組み立てれば:

data:text/plain;base64,TWFu

このurlをブラウザにコピペしてアクセスしたら、'Man'が表示されるのは確認できます。

ArrayBuffer

でもatobbtoaは文字列の操作メソッドですね。バイナリーに対して、まず文字列に変換する必要があります。

Fileなら、FileReaderが使える。CanvasならCanvas.toDataURL()を使いましょう。

JavaScriptはもうArrayBufferをサポートしています。ArrayBufferは汎用のバイナリー構造で、直接操作できなくて、 DataViewを通じていじることができます。またはArrayBuffer出なくて、ある特定なフォーマットがついてるTypedArray(Int8Arrayなど)を使えば簡単に操作できます。

うん、また'Man'を見てみましょう。

// 3 Byte,24 bit
const buffer = new Int8Array(3);
buffer[0] = 77;
buffer[1] = 97;
buffer[2] = 110;

このbufferは'Man'のバイナリー構造ですね。chromeならTextDecoderTextEncoderをサポートしていますので、使ってみましょう。

const encoder = new TextEncoder();
const encoded = encoder.encode('Man');
buffer.forEach((num,i) => {
    console.assert(buffer[i] === encoded[i]);
});

const decoder = new TextDecoder();
console.assert(decoder.decode(buffer) === 'Man');

TextDecoderなどサポートしてない場合はどうするの?これも簡単です。 逆戻りで8-bitづつ文字列に変換すればいいです(でもasc文字だけ)。String.fromCharCodeを使います。

String.fromCharCode(77)
// 'M'

もしArrayBufferを使うなら?

//24bitを初期化
const buffer = new ArrayBuffer(24);

//DataViewを用意
const view = new DataView(buffer,0,24); 

//各オフセットにデータを入れる
view.setInt8(0, 77);
view.setInt8(8, 97);
view.setInt8(16, 110);

// これで全く同じなバイナリーができた、確認してみよう
const decoder = new TextDecoder();
console.assert(decoder.decode(buffer) === 'Man');

まとめ

ArrayBufferはバイナリー構造です。low levelなので、DataViewを使って操作します。 TypedArrayはその上の便利なデータ構造です。

バイナリデータを8-bitづつ数字に変換すれば、文字列へ変換できます。 自分でやってもいいし、TextDecoderなどを使えばもっと便利になります。

ただ文字列についてUnicodeの問題があり、8-bitづつっていうルールは実際ダメです。 unicodeについて、自分が書いたjs正規表現を理解してみたをみてはおすすめです。