JavaScriptでBase64エンコードする

NodeはBufferにbase64エンコード機能があるのだが

Buffer.from("あいうえお", "utf-8").toString("base64")

ブラウザのJSにはこういう便利な実装がないのだった。どうするか?

window.btoa

ブラウザにはbtoaというbase64エンコードを行う関数がある。しかしこいつはascii文字列またはBinary Stringしか受け付けず、日本語を渡すと例外を投げるという英語圏仕様のクソ関数。このためbtoaを使うには一度binary stringに変換しなくてはならない。

文字列からbinary stringに変換する最も簡単な方法は、URLエンコードしてunescape

const str = "🐱";
const binStr = unescape(encodeURIComponent(str))
btoa(binStr);

FileReader#readAsBinaryStringは、FileやBlobをbinary stringに変換するメソッド。これで変換してbtoaに渡せばbase64文字列を得られる。

const blob = new Blob(["🐱"]);
const fr = new FileReader()
fr.readAsBinaryString(blob);
fr.onload = () => {
  btoa(fr.result);
};

String.fromCharCodeを使うとUint8Arrayからbinary stringに変換できる。文字列からUint8Arrayに変換するにはTextEncoderというオブジェクトを使う。

const str = "🐱";
const uint8arr = new TextEncoder("utf-8").encode(str);
const binStr = Array.from(uint8arr).map(b => String.fromCharCode(b)).join("");
btoa(binStr);

binary stringはTyped Arrayが実装される以前のバイナリを扱うためのレガシーな形式なので、できればUint8Arrayのまま処理したい。

readAsDataURLを使う

FileReader#readAsDataURLは FileやBlobをデータURIスキームに変換するメソッドである。

data:application/octet-stream;base64,8J+QsQ==

データ部分はbase64エンコードされるので、そこだけ切り出せば良い。

const blob = new Blob(["🐱"]);
const fr = new FileReader()
fr.readAsDataURL(blob);
fr.onload = () => {
  const r = fr.result;
  r.slice(r.indexOf(',') + 1);
};

base64エンコーダーを実装する

Uint8Arrayを直接base64にできないなら自分でエンコード処理を書いてしまえ。

function encodeBase64(bytes) {
  const table = [];
  for (let i = 65; i < 91; i++) table.push(String.fromCharCode(i))
  for (let i = 97; i < 123; i++) table.push(String.fromCharCode(i))
  for (let i = 0; i < 10; i++) table.push(i.toString(10))
  table.push("+");
  table.push("/");

  let base64 = "";
  let i = 0;
  const len = bytes.byteLength;
  for (i = 0; i < len; i += 3) {
    if (len === i + 1) {  // last 1 byte
      const a = (bytes[i] & 0xfc) >> 2;
      const b = ((bytes[i] & 0x03) << 4);
      base64 += table[a];
      base64 += table[b];
      base64 += "==";
    } else if (len === i + 2) { // last 2 bytes
      const a = (bytes[i] & 0xfc) >> 2;
      const b = ((bytes[i] & 0x03) << 4) | ((bytes[i+1] & 0xf0) >> 4);
      const c = ((bytes[i+1] & 0x0f) << 2);
      base64 += table[a];
      base64 += table[b];
      base64 += table[c];
      base64 += "=";
    } else {
      const a = (bytes[i] & 0xfc) >> 2;
      const b = ((bytes[i] & 0x03) << 4) | ((bytes[i+1] & 0xf0) >> 4);
      const c = ((bytes[i+1] & 0x0f) << 2) | ((bytes[i+2] & 0xc0) >> 6);
      const d = bytes[i+2] & 0x3f;
      base64 += table[a];
      base64 += table[b];
      base64 += table[c];
      base64 += table[d];
    }
  }
  return base64;
}

const uint8Arr = Uint8Array.of(0xF0, 0x9F, 0x90, 0xB1);
encodeBase64(uint8arr);

const uint8arr2 = new TextEncoder("utf-8").encode("🐱");
encodeBase64(uint8arr2);

base64 encode/decode using Uint8Array on browser · GitHub