てもとツール

2026-05-15

Base64で日本語が文字化けする理由とUTF-8

JavaScriptのbtoaで日本語をBase64エンコードすると文字化けやエラーが起きます。UTF-8バイト列への変換が必要な理由と、TextEncoderを使った正しい実装を解説します。

この記事の要点

  • btoa()はASCII範囲外の文字(日本語など)を受け取るとエラーを投げる
  • JavaScriptの文字列は内部的にUTF-16で管理され、btoaはLatin-1のみ対応
  • 正しい手順はTextEncoderでUTF-8バイト列に変換してからBase64エンコードする
  • デコードも逆順でBase64デコード後にTextDecoderでUTF-8文字列に変換する
  • URL-safe Base64はURLパラメーターやJWTで必要になる変換形式

btoa('日本語') を実行すると InvalidCharacterError が発生します。この動作を知らずに実装すると、日本語テキストが文字化けしたり例外が投げられたりして、原因の特定に時間を取られます。

非エンジニアの利用者にとっても無縁ではありません。「日本語を含む設定ファイルをコピーして使ったら壊れた」という事例は現場で頻繁に起きます。この記事では文字化けの根本原因と正しい実装パターンを解説します。

btoaがエラーを投げる理由

btoa(Binary to ASCII)はブラウザ組み込みのBase64エンコード関数です。仕様上 Latin-1(ISO-8859-1)の文字のみを受け付けます。Latin-1は0x00〜0xFFの256文字しか持たず、日本語・中国語・絵文字など多バイト文字は含まれません。

btoa('hello');     // 'aGVsbG8='  ← ASCII文字は問題なし
btoa('こんにちは'); // Uncaught InvalidCharacterError
btoa('😀');        // Uncaught InvalidCharacterError

JavaScriptの文字列は内部的にUTF-16で管理されています。'こ'はUTF-16ではU+3053(2バイト)です。Latin-1の範囲(1バイト)を超えているため、btoaは処理を拒否します。

エラーメッセージは次のように表示されます。

The string to be encoded contains characters outside of the Latin1 range.

UTF-16とUTF-8の違い

文字化けを理解するには、2つのエンコード方式の違いを把握する必要があります。JavaScriptが内部で使うUTF-16と、ネットワーク/ファイルの標準であるUTF-8です。

エンコード’a’(U+0061)‘あ’(U+3042)’😀‘(U+1F600)
UTF-162バイト: 00 612バイト: 30 424バイト(サロゲートペア): D8 3D DE 00
UTF-81バイト: 613バイト: E3 81 824バイト: F0 9F 98 80

JavaScriptのString.lengthはUTF-16コード単位の数を返します。そのため'あ'.length === 1ですが、内部では2バイトです。btoaはこのUTF-16表現をそのまま受け取ろうとし、Latin-1の範囲を超える文字でエラーを出します。

Base64はバイナリデータを安全にテキスト転送するための方式です。「どのエンコードのバイト列を渡すか」が正しさのすべてです。Webの標準はUTF-8のため、日本語テキストはUTF-8バイト列に変換してからBase64エンコードします。


正しいエンコード手順

ステップ1: TextEncoderでUTF-8バイト列に変換

TextEncoderはECMAScript標準APIで、文字列をUTF-8のUint8Arrayに変換します。

const encoder = new TextEncoder();
const bytes = encoder.encode('こんにちは');
// Uint8Array(15) [227, 129, 147, 227, 130, 147, ...]
// 'こ'は E3 81 93 の3バイト × 5文字 = 15バイト

ステップ2: Uint8ArrayをLatin-1文字列に変換してbtoaに渡す

btoaはバイト値の配列(各要素が0〜255のバイナリ)を受け付けます。Uint8Arrayはそのまま渡せません。String.fromCharCodeを使って、1バイト=1文字のLatin-1文字列に変換します。

function encodeBase64(input) {
  const bytes = new TextEncoder().encode(input);
  const latin1 = String.fromCharCode(...bytes);
  return btoa(latin1);
}

encodeBase64('こんにちは');
// '44GT44KT44Gr44Gh44Gv'

長い文字列での注意点

String.fromCharCode(...bytes) のスプレッド展開は、バイト数が多いとスタックオーバーフローが発生します。数十万バイト以上を扱う場合は for ループを使います。

function encodeBase64Safe(input) {
  const bytes = new TextEncoder().encode(input);
  let latin1 = '';
  for (let i = 0; i < bytes.length; i++) {
    latin1 += String.fromCharCode(bytes[i]);
  }
  return btoa(latin1);
}

正しいデコード手順

デコードはエンコードの逆順です。

function decodeBase64(base64) {
  const latin1 = atob(base64);
  const bytes = Uint8Array.from(latin1, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

decodeBase64('44GT44KT44Gr44Gh44Gv');
// 'こんにちは'

atob で Base64 から Latin-1 文字列に戻します。次に charCodeAt(0) で各文字のバイト値を取り出して Uint8Array を作ります。最後に TextDecoder で UTF-8 文字列に復元します。


モダンな代替: Uint8Array.toBase64(ES2025)

ECMAScript 2025 では、toBase64()fromBase64()Uint8Array に追加されました。これらを使うとより簡潔に書けます。

// エンコード
const bytes = new TextEncoder().encode('こんにちは');
const base64 = bytes.toBase64(); // '44GT44KT44Gr44Gh44Gv'

// デコード
const decoded = Uint8Array.fromBase64(base64);
const text = new TextDecoder().decode(decoded); // 'こんにちは'

Chrome 132+、Firefox 133+、Node.js 22+ で利用可能です。古い環境へのフォールバックが必要な場合はTextEncoder + btoaパターンを使います。


URL-safe Base64

標準のBase64は+(プラス)と/(スラッシュ)を使います。URLのクエリパラメーターに含める場合、これらはURLエンコードが必要です。URL-safe Base64では+-/_に置き換え、末尾の=パディングを省略します。

function encodeBase64Url(input) {
  return encodeBase64(input)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

function decodeBase64Url(base64url) {
  const base64 = base64url
    .replace(/-/g, '+')
    .replace(/_/g, '/')
    .padEnd(base64url.length + (4 - base64url.length % 4) % 4, '=');
  return decodeBase64(base64);
}

JWT(JSON Web Token)のペイロードはURL-safe Base64でエンコードされています。JWTのeyJhbG...という見慣れた文字列はこの形式です。


Base64の用途と注意点

Base64はデータを膨らませるエンコードです。3バイトを4文字に変換するため、サイズが約33%増加します。

用途詳細
メール添付(MIME)SMTPはASCIIテキストのみ対応のため画像をBase64変換
CSSのData URIurl("data:image/png;base64,...") で画像をインライン埋め込み
JWTのペイロードヘッダー・ペイロードはURL-safe Base64でエンコード
APIのバイナリ転送JSON中にバイナリデータを含めるときの標準手段
Basic認証Authorization: Basic <base64(user:pass)> の形式

てもとツールのBase64ツール

Base64エンコード・デコードツールでは、TextEncoderを使った正しいUTF-8ベースの変換を実装しています。日本語・絵文字・特殊記号を含むあらゆるテキストを文字化けなしにエンコード・デコードできます。

処理はすべてブラウザ内で完結するため、入力したテキストがサーバーに送信されることはありません。機密性の高いデータのBase64変換でも安心して利用できます。


まとめ

Base64で日本語が文字化けする根本原因は、btoaがLatin-1(1バイト文字)しか受け付けないことです。対処は「TextEncoderでUTF-8バイト列に変換してからbtoaに渡す」の一点です。

正しい変換関数をまとめます。

// エンコード(安全版)
function encodeBase64(text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

// デコード
function decodeBase64(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

モダン環境ではUint8Array.toBase64()/fromBase64()が利用可能です。ブラウザサポートを確認した上で採用を検討してください。