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-16 | 2バイト: 00 61 | 2バイト: 30 42 | 4バイト(サロゲートペア): D8 3D DE 00 |
| UTF-8 | 1バイト: 61 | 3バイト: E3 81 82 | 4バイト: 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 URI | url("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()が利用可能です。ブラウザサポートを確認した上で採用を検討してください。