--- title: Base64 のエンコードとデコード slug: Glossary/Base64 tags: - Advanced - Base64 - JavaScript - Typed Arrays - URI - URL - Unicode Problem - atob() - btoa() translation_of: Glossary/Base64 original_slug: Web/API/WindowBase64/Base64_encoding_and_decoding ---

Base64 とは、バイナリーからテキストへの符号化を行う手法のグループであり、64 を基数とする表現に変換することで、バイナリーデータを ASCII 文字列で表すことができます。Base64 という呼び方は、MIME の Content-Transfer-Encoding における特定の符号化方式の名前に由来します。

Base64 符号化方式がよく使われるのは、テキストデータを扱うよう設計されたメディア上で、バイナリーデータを格納または転送する必要がある場合です。Base64 符号化により、転送中に変換されることなく、バイナリーデータがそのままであることを保証できます。Base64 は、MIME による電子メールや XML における複合型データの格納など、多くのアプリケーションで幅広く使われています。

JavaScript には、Base64 文字列のエンコードとデコードのそれぞれに対応した、次の 2 つの関数があります。

atob() 関数は、Base64 符号化方式によりエンコードされている文字列をデコードしてバイナリー文字列を作ります。逆に btoa() 関数は、バイナリー文字列から Base64 でエンコードされた ASCII 文字列を作ります。

atob()btoa() のどちらも、文字列に対して動作します。もし ArrayBuffer に対して動作させたい場合は、この段落 を読んでください。

符号化によるサイズ増加

Base64 の 1 文字はデータのちょうど 6 ビット分を表します。そのため、入力される文字列やバイナリーファイルに含まれる 3 バイト (3×8 ビット = 24 ビット) は、4 桁の Base64 で表されます (4×6 = 24 ビット)。

このことにより、Base64 で表された文字列またはファイルは、元のサイズの 133% の大きさになると言えます (33% の増加)。エンコードされるデータが小さい場合は、さらに増加幅が大きくなります。例えば、length === 1 である文字列 "a" は、エンコードされて length === 4 の文字列 "YQ==" になり、これは 300% の増加です。

参考文書

データ URL
データ URL は、RFC 2397 により定義されており、 これにより文書中に小さなファイルを埋め込むことができます。
Base64
ウィキペディアの Base64 符号化方式に関する記事です。
WindowOrWorkerGlobalScope ミックスイン
atobbtoa を規定し、これらは RFC 4648 により規定された Base64 にエンコードすると定めています。
RFC 4648
セクション 4 で Base64 のアルゴリズムを規定し、またセクション 5 で URL 向けの "base64url" アルゴリズム (こちらは atobbtoa では使われない) も定義しています。
{{domxref("WindowBase64.atob","atob()")}}
Base64 によりエンコードされている ASCII 文字列をデコードして、バイナリー文字列を作ります。
{{domxref("WindowBase64.btoa","btoa()")}}
バイナリー文字列から、Base64 によりエンコードされた ASCII 文字列を作ります。
あの「Unicode の問題」
ほとんどのブラウザーでは、Unicode 文字列を使って btoa() を実行すると、Character Out Of Range 例外が発生します。この段落では、これに対するいくつかの対策を説明しています。
URIScheme
Mozilla のサポートした URI スキームのリスト
StringView
この記事では、次を狙いとしたライブラリーを公開しています
  • 文字列に対する C 言語に似たインターフェイス (すなわち文字のコードの配列であり、JavaScript では ArrayBufferView) を JavaScript の ArrayBuffer インターフェイスを使って作ること
  • 文字列に似たオブジェクト (これからは stringView) 向けの、不変である JavaScript 文字列に対してではなく必ず数値の配列に対して働く、メソッドのコレクションを作ること
  • JavaScript デフォルトである UTF-16 の DOMString 以外の Unicode でも動作すること

全て表示...

ツール

全て表示...

あの「Unicode の問題」

DOMString は 16 ビットで符号化された文字列であるので、Unicode 文字列を使って window.btoa を実行すると、8 ビットの範囲 (0x00~0xFF) を超えた文字がある場合に、ほとんどのブラウザーで Character Out Of Range 例外が発生します。以下は、この問題を解決するための 5 つの方法です。

方法 1 – JavaScript の UTF-16 => Base64

Unicode 問題を解決する、非常に高速で幅広く使われている方法は、JavaScript のネイティブ UTF-16 文字列を直接 Base64 にエンコードすることです。デモのために URL data:text/plain;charset=utf-16;base64,OCY5JjomOyY8Jj4mPyY= を開いてください (このデータ URL をコピーし、新しいタブを開き、データ URL をアドレスバーに貼り付け、エンターをを押す)。この方法は、文字列を配列に割り当てるところを除き、どのような種類の変換も必要としないため、特に効率的です。次のコードは、Base64 文字列から ArrayBuffer に変換したり、その逆変換をするのにも便利です (下記参照)。

"use strict";

/*\
|*|
|*|  Base64 / binary data / UTF-8 strings utilities (#1)
|*|
|*|  https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*|  Author: madmurphy
|*|
\*/

/* Array of bytes to base64 string decoding */

function b64ToUint6 (nChr) {

  return nChr > 64 && nChr < 91 ?
      nChr - 65
    : nChr > 96 && nChr < 123 ?
      nChr - 71
    : nChr > 47 && nChr < 58 ?
      nChr + 4
    : nChr === 43 ?
      62
    : nChr === 47 ?
      63
    :
      0;

}

function base64DecToArr (sBase64, nBlockSize) {

  var
    sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
    nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

  for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
    nMod4 = nInIdx & 3;
    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
    if (nMod4 === 3 || nInLen - nInIdx === 1) {
      for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
        aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
      }
      nUint24 = 0;
    }
  }

  return aBytes;
}

/* Base64 string to array encoding */

function uint6ToB64 (nUint6) {

  return nUint6 < 26 ?
      nUint6 + 65
    : nUint6 < 52 ?
      nUint6 + 71
    : nUint6 < 62 ?
      nUint6 - 4
    : nUint6 === 62 ?
      43
    : nUint6 === 63 ?
      47
    :
      65;

}

function base64EncArr (aBytes) {

  var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = "";

  for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
    nMod3 = nIdx % 3;
    /* Uncomment the following line in order to split the output in lines 76-character long: */
    /*
    if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
    */
    nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
    if (nMod3 === 2 || aBytes.length - nIdx === 1) {
      sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
      nUint24 = 0;
    }
  }

  return  eqLen === 0 ?
      sB64Enc
    :
      sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "==");

}

テスト

var myString = "☸☹☺☻☼☾☿";

/* Part 1: `myString` をネイティブの UTF-16 を使って Base64 にエンコードする */

var aUTF16CodeUnits = new Uint16Array(myString.length);
Array.prototype.forEach.call(aUTF16CodeUnits, function (el, idx, arr) { arr[idx] = myString.charCodeAt(idx); });
var sUTF16Base64 = base64EncArr(new Uint8Array(aUTF16CodeUnits.buffer));

/* 出力を表示する */

alert(sUTF16Base64); // "OCY5JjomOyY8Jj4mPyY="

/* Part 2: `sUTF16Base64` を UTF-16 にデコードする */

var sDecodedString = String.fromCharCode.apply(null, new Uint16Array(base64DecToArr(sUTF16Base64, 2).buffer));

/* 出力を表示する */

alert(sDecodedString); // "☸☹☺☻☼☾☿"

生成された Base64 文字列はどこでも使えますが、UTF-16 で表現されています。もし UTF-8 を望む場合は次の方法を参照してください。

方法 1 に対する補足: Base64 文字列を Uint8ArrayArrayBuffer にデコードする

上記の関数を使って、Base64 でエンコードされた文字列から Uint8ArrayArrayBuffer を作ることもできます。

var myArray = base64DecToArr("QmFzZSA2NCDigJQgTW96aWxsYSBEZXZlbG9wZXIgTmV0d29yaw=="); // "Base 64 \u2014 Mozilla Developer Network" (as UTF-8)

var myBuffer = base64DecToArr("QmFzZSA2NCDigJQgTW96aWxsYSBEZXZlbG9wZXIgTmV0d29yaw==").buffer; // "Base 64 \u2014 Mozilla Developer Network" (as UTF-8)

alert(myBuffer.byteLength);
注意: 関数 base64DecToArr(sBase64[, nBlockSize]) は、8 ビットの Uint8Array を返します。もし 16 ビット / 32 ビット / 64 ビットの生データのバッファを作ることが目的であれば、引数 nBlockSize を使ってください。これはバイト数であり、Uint8Array.buffer.bytesLength プロパティはその倍数になります (1 や省略された場合は ASCII、バイナリーデータ、バイナリー文字列、UTF-8 文字列向けです。2 は UTF-16 文字列向け、4 は UTF-32 文字列向けです)。

完全なライブラリーは StringView – 型付き配列に基づく C 言語に似た文字列表現 を参照してください (ソースコードは GitHub で利用できます)。

方法 2 – JavaScript の UTF-16 => UTF-8 => Base64

この方法は、JavaScript ネイティブの UTF-16 文字列を UTF-8 文字列に変換し、それを Base64 でエンコードします。これにより、純粋な ASCII 文字列から Base64 への変換は、ネイティブの btoa() のように、常に同じ結果を出力します。

"use strict";

/*\
|*|
|*|  Base64 / binary data / UTF-8 strings utilities (#2)
|*|
|*|  https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*|  Author: madmurphy
|*|
\*/

/* Array of bytes to base64 string decoding */

function b64ToUint6 (nChr) {

  return nChr > 64 && nChr < 91 ?
      nChr - 65
    : nChr > 96 && nChr < 123 ?
      nChr - 71
    : nChr > 47 && nChr < 58 ?
      nChr + 4
    : nChr === 43 ?
      62
    : nChr === 47 ?
      63
    :
      0;

}

function base64DecToArr (sBase64, nBlockSize) {

  var
    sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
    nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

  for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
    nMod4 = nInIdx & 3;
    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
    if (nMod4 === 3 || nInLen - nInIdx === 1) {
      for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
        aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
      }
      nUint24 = 0;
    }
  }

  return aBytes;
}

/* Base64 string to array encoding */

function uint6ToB64 (nUint6) {

  return nUint6 < 26 ?
      nUint6 + 65
    : nUint6 < 52 ?
      nUint6 + 71
    : nUint6 < 62 ?
      nUint6 - 4
    : nUint6 === 62 ?
      43
    : nUint6 === 63 ?
      47
    :
      65;

}

function base64EncArr (aBytes) {

  var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = "";

  for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) {
    nMod3 = nIdx % 3;
    /* Uncomment the following line in order to split the output in lines 76-character long: */
    /*
    if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; }
    */
    nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24);
    if (nMod3 === 2 || aBytes.length - nIdx === 1) {
      sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63));
      nUint24 = 0;
    }
  }

  return  eqLen === 0 ?
      sB64Enc
    :
      sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "==");

}

/* UTF-8 array to DOMString and vice versa */

function UTF8ArrToStr (aBytes) {

  var sView = "";

  for (var nPart, nLen = aBytes.length, nIdx = 0; nIdx < nLen; nIdx++) {
    nPart = aBytes[nIdx];
    sView += String.fromCharCode(
      nPart > 251 && nPart < 254 && nIdx + 5 < nLen ? /* six bytes */
        /* (nPart - 252 << 30) may be not so safe in ECMAScript! So...: */
        (nPart - 252) * 1073741824 + (aBytes[++nIdx] - 128 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
      : nPart > 247 && nPart < 252 && nIdx + 4 < nLen ? /* five bytes */
        (nPart - 248 << 24) + (aBytes[++nIdx] - 128 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
      : nPart > 239 && nPart < 248 && nIdx + 3 < nLen ? /* four bytes */
        (nPart - 240 << 18) + (aBytes[++nIdx] - 128 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
      : nPart > 223 && nPart < 240 && nIdx + 2 < nLen ? /* three bytes */
        (nPart - 224 << 12) + (aBytes[++nIdx] - 128 << 6) + aBytes[++nIdx] - 128
      : nPart > 191 && nPart < 224 && nIdx + 1 < nLen ? /* two bytes */
        (nPart - 192 << 6) + aBytes[++nIdx] - 128
      : /* nPart < 127 ? */ /* one byte */
        nPart
    );
  }

  return sView;

}

function strToUTF8Arr (sDOMStr) {

  var aBytes, nChr, nStrLen = sDOMStr.length, nArrLen = 0;

  /* mapping... */

  for (var nMapIdx = 0; nMapIdx < nStrLen; nMapIdx++) {
    nChr = sDOMStr.charCodeAt(nMapIdx);
    nArrLen += nChr < 0x80 ? 1 : nChr < 0x800 ? 2 : nChr < 0x10000 ? 3 : nChr < 0x200000 ? 4 : nChr < 0x4000000 ? 5 : 6;
  }

  aBytes = new Uint8Array(nArrLen);

  /* transcription... */

  for (var nIdx = 0, nChrIdx = 0; nIdx < nArrLen; nChrIdx++) {
    nChr = sDOMStr.charCodeAt(nChrIdx);
    if (nChr < 128) {
      /* one byte */
      aBytes[nIdx++] = nChr;
    } else if (nChr < 0x800) {
      /* two bytes */
      aBytes[nIdx++] = 192 + (nChr >>> 6);
      aBytes[nIdx++] = 128 + (nChr & 63);
    } else if (nChr < 0x10000) {
      /* three bytes */
      aBytes[nIdx++] = 224 + (nChr >>> 12);
      aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
      aBytes[nIdx++] = 128 + (nChr & 63);
    } else if (nChr < 0x200000) {
      /* four bytes */
      aBytes[nIdx++] = 240 + (nChr >>> 18);
      aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
      aBytes[nIdx++] = 128 + (nChr & 63);
    } else if (nChr < 0x4000000) {
      /* five bytes */
      aBytes[nIdx++] = 248 + (nChr >>> 24);
      aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
      aBytes[nIdx++] = 128 + (nChr & 63);
    } else /* if (nChr <= 0x7fffffff) */ {
      /* six bytes */
      aBytes[nIdx++] = 252 + (nChr >>> 30);
      aBytes[nIdx++] = 128 + (nChr >>> 24 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 18 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 12 & 63);
      aBytes[nIdx++] = 128 + (nChr >>> 6 & 63);
      aBytes[nIdx++] = 128 + (nChr & 63);
    }
  }

  return aBytes;

}

テスト

/* テスト */

var sMyInput = "Base 64 \u2014 Mozilla Developer Network";

var aMyUTF8Input = strToUTF8Arr(sMyInput);

var sMyBase64 = base64EncArr(aMyUTF8Input);

alert(sMyBase64); // "QmFzZSA2NCDigJQgTW96aWxsYSBEZXZlbG9wZXIgTmV0d29yaw=="

var aMyUTF8Output = base64DecToArr(sMyBase64);

var sMyOutput = UTF8ArrToStr(aMyUTF8Output);

alert(sMyOutput); // "Base 64 — Mozilla Developer Network"

方法 3 – JavaScript の UTF-16 => バイナリー文字列 => Base64

これは、最も速く最もコンパクトな方法です。出力は方法 1 (UTF-16 ででエンコードされた文字列) のものと全く同じですが、{{domxref("WindowBase64.atob","atob()")}} と {{domxref("WindowBase64.btoa","btoa()")}} を書き直すのではなく、ネイティブのものを使います。この方法はエンコードまたはデコードの入力として、型付き配列の代わりに、中間フォーマットであるバイナリー文字列を使います。方法 1 (バイナリー文字列 は灰色の領域です) に比べると、これは「汚い」回避策ではありますが、問題なく動作し、必要なコードはわずか数行です。

"use strict";

/*\
|*|
|*|  Base64 / binary data / UTF-8 strings utilities (#3)
|*|
|*|  https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*|  Author: madmurphy
|*|
\*/

function btoaUTF16 (sString) {

	var aUTF16CodeUnits = new Uint16Array(sString.length);
	Array.prototype.forEach.call(aUTF16CodeUnits, function (el, idx, arr) { arr[idx] = sString.charCodeAt(idx); });
	return btoa(String.fromCharCode.apply(null, new Uint8Array(aUTF16CodeUnits.buffer)));

}

function atobUTF16 (sBase64) {

	var sBinaryString = atob(sBase64), aBinaryView = new Uint8Array(sBinaryString.length);
	Array.prototype.forEach.call(aBinaryView, function (el, idx, arr) { arr[idx] = sBinaryString.charCodeAt(idx); });
	return String.fromCharCode.apply(null, new Uint16Array(aBinaryView.buffer));

}

テスト

var myString = "☸☹☺☻☼☾☿";

/* Part 1: `myString` をネイティブの UTF-16 を使って Base64 にエンコードする */

var sUTF16Base64 = btoaUTF16(myString);

/* 出力を表示する */

alert(sUTF16Base64); // "OCY5JjomOyY8Jj4mPyY="

/* Part 2: `sUTF16Base64` を UTF-16 にデコードする */

var sDecodedString = atobUTF16(sUTF16Base64);

/* 出力を表示する */

alert(sDecodedString); // "☸☹☺☻☼☾☿"

バイナリー文字列の代わりに型付き配列を使う、よりクリーンな方法については、方法 1方法 2 を参照してください。

方法 4 – エンコード前に文字列をエスケープ処理する

function b64EncodeUnicode(str) {
    // 最初に encodeURIComponent を使って "%" でエンコードされた UTF-8 文字列を取得し、
    // 次に "%" でエンコードされた文字列をバイナリー文字列に変換し、
    // それを btoa に与えます。
    return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
        function toSolidBytes(match, p1) {
            return String.fromCharCode('0x' + p1);
    }));
}

b64EncodeUnicode('✓ à la mode'); // "4pyTIMOgIGxhIG1vZGU="
b64EncodeUnicode('\n'); // "Cg=="

Base64 でエンコードされた値を元の文字列に戻すには、次のようにします。

function b64DecodeUnicode(str) {
    // 逆変換: バイナリー文字列から "%" エンコードへ、そしてオリジナルの文字列へ。
    return decodeURIComponent(atob(str).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
}

b64DecodeUnicode('4pyTIMOgIGxhIG1vZGU='); // "✓ à la mode"
b64DecodeUnicode('Cg=='); // "\n"

Unibabel は、この方法を使った共通の変換を実装しています。

方法 5 – DOM の atob()btoa() を JavaScript の TypedArray と UTF-8 を使って書き換える

TextEncoder のポリフィル、例えば TextEncoding (レガシーの Windows、Mac、ISO のエンコーディングも含む) や TextEncoderLite を、モダンブラウザーと Node.js の両方で使える Buffer または base64-jsTypeScript 版の base64-js のような Base64 実装とを、組み合わせて使います。

ネイティブの TextEncoder 実装がない場合、最も軽量な方法は 方法 3 でしょう。なぜなら、とても高速であることに加え、方法 3 は標準状態の IE9 でも動作するからです。 もう一つの方法は、TextEncoderLitebase64-js を使うことです。可能な場合はブラウザーの実装を使ってください。

次の関数は、この考えを実装したものです。これは、base64-js が <script type="text/javascript" src="base64js.min.js"/> のようにインポートされていることを前提にしています。TextEncoderLite は UTF-8 でのみ機能することに注意してください。

function Base64Encode(str, encoding = 'utf-8') {
    var bytes = new (typeof TextEncoder === "undefined" ? TextEncoderLite : TextEncoder)(encoding).encode(str);
    return base64js.fromByteArray(bytes);
}

function Base64Decode(str, encoding = 'utf-8') {
    var bytes = base64js.toByteArray(str);
    return new (typeof TextDecoder === "undefined" ? TextDecoderLite : TextDecoder)(encoding).decode(bytes);
}

Note: TextEncoderLite は、4 バイトの UTF-8 文字、つまり '\uD842\uDFB7' や '\u{20BB7}' のような文字を誤って解釈します。この Issue を参照してください。
あるいは、代わりに text-encoding を使ってください。

いくつかの場合には、UTF-8 に変換した後 Base64 にする上記の方法は、記憶領域に対してとても非効率的です。U+0800 から U+FFFF の範囲にある文字は、UTF-8 では 3 バイトにエンコードされますが UTF-16 では 2 バイトであり、これらがテキストの大部分を占める場合、UTF-8 の出力長は UTF-16 よりも長くなります。均等に分散した UTF-16 コードポイントを含む JavaScript 文字列の場合、Base64 の変換の前のエンコードを UTF-8 ではなく UTF-16 にすることで、サイズを 40% 減少できます。