文字列のエンコードとデコード
Dart の文字列は内部的に UTF-16 なので、エンコードの前に utf8.encode で UTF-8 のバイト列に変換します。
import 'dart:convert';
void main() {
final enc = base64Encode(utf8.encode('héllo wörld'));
print(enc); // aMOpbGxvIHfDtnJsZA==
print(utf8.decode(base64Decode(enc))); // héllo wörld
}
base64Encode(bytes) は base64.encode(bytes) の、base64Decode(s) は base64.decode(s) の省略形です。デコードしたバイト列が UTF-8 でない場合、utf8.decode は FormatException: Invalid UTF-8 byte をスローします。代わりに置換文字を得たいなら allowMalformed: true を渡してください。
同じ 2 つの呼び出しで、たとえば package:http 用の HTTP Basic 認証ヘッダーも作れます。
final headers = {'Authorization': 'Basic ${base64Encode(utf8.encode('user:pass'))}'};
// Authorization: Basic dXNlcjpwYXNz
Basic Auth ヘッダージェネレーター なら、任意の認証情報について同じ値を表示できます。
URLセーフな Base64 と normalize()
base64Url は -_ のアルファベットを使う 2 つ目のコーデックです。どちらのエンコーダーも常にパディングを付けるので、形式上不要なら取り除いてください。
final d = utf8.encode('<<???>>');
print(base64Url.encode(d)); // PDw_Pz8-Pg==
print(base64Url.encode(d).replaceAll('=', '')); // PDw_Pz8-Pg
デコードは、Dart が多くの言語より親切な部分です。どちらのデコーダーも両方のアルファベットを受け付けますが、正しいパディングが必要です。normalize はパディングを補い、アルファベットも変換します。
base64Url.normalize('PDw_Pz8-Pg'); // PDw_Pz8-Pg==
base64.normalize('PDw_Pz8-Pg'); // PDw/Pz8+Pg==
utf8.decode(base64Url.decode(base64Url.normalize('PDw_Pz8-Pg'))); // <<???>>
utf8.decode(base64Decode('PDw_Pz8-Pg==')); // <<???>>
normalize は入力が有効な Base64 でなければ FormatException をスローするので、バリデーターとしても使えます。JWT のセグメントはパディングなしの Base64URL です。Base64 と Base64URL の違い と パディングの解説 をご覧いただくか、JWT デコーダー に貼り付けてみてください。
normalize があれば、JWT のペイロードは 1 行で読めます。
const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig';
final payload = utf8.decode(base64Url.decode(base64Url.normalize(jwt.split('.')[1])));
print(payload); // {"sub":"1234"}
print(jsonDecode(payload)['sub']); // 1234
これはクレームを読むだけで、署名は検証しません。クライアント側の認可判断に使ってはいけません。
ファイルのエンコードとデコード
import 'dart:io';
final bytes = await File('hello.txt').readAsBytes();
print(base64Encode(bytes)); // aGVsbG8K
await File('copy.txt').writeAsBytes(base64Decode('aGVsbG8K'));
大きなファイルのストリーム処理
base64.encoder と base64.decoder はストリームトランスフォーマーなので、ファイルをチャンクごとに変換できます。
// encode: bytes -> Base64 text -> ASCII bytes on disk
await File('big.bin')
.openRead()
.transform(base64.encoder)
.transform(ascii.encoder)
.pipe(File('big.b64').openWrite());
// decode: ASCII bytes -> String chunks -> bytes
await File('big.b64')
.openRead()
.transform(ascii.decoder)
.transform(base64.decoder)
.pipe(File('big.out').openWrite());
トランスフォーマーはチャンク間で余ったバイトを持ち越すため、チャンクのサイズは関係ありません。300 KB のテストファイルを使うと、どちらの方向でもメモリ上の関数とバイト単位で同じ結果になりました。
Flutter:画像と Data URI
Base64 の画像を表示するには次のようにします。
Image.memory(base64Decode(b64))
アセットや選択したファイルから Base64 を作るには、バイト列を読み込んで base64Encode を呼びます。Data URI の接頭辞は UriData が処理してくれます。
UriData.parse('data:text/plain;base64,aGk=').contentAsBytes(); // [104, 105]
UriData.fromBytes(pngBytes, mimeType: 'image/png').toString(); // data:image/png;base64,...
Data URI をそのまま base64Decode に渡すと、data の後の : で Invalid character (at character 5) になります。正体のわからない文字列の中身を確かめるなら、Base64 から画像への変換 でブラウザ上でプレビューできます。
検証と FormatException のメッセージ
デコードのエラーはすべて FormatException です。toString() にはメッセージ、入力、問題箇所を示すキャレットが含まれ、e.message と e.offset で個別に取得できます。base64Decode の場合:
| 入力 | メッセージ |
|---|---|
aGk | Invalid length, must be multiple of four (at character 4) |
a | Invalid length, must be multiple of four (at character 2) |
aGk=!! | Invalid padding character (at character 5) |
aGk== | Invalid padding character (at character 5) |
aGl= | Invalid encoding before padding (at character 4) |
"aGVs\nbG8K" | Invalid character (at character 5) |
多くの言語が寛容な 2 点について、Dart は厳格です。改行を拒否し(MIME や PEM の入力では取り除いてください)、0 でないパディングビット(aGl=)も拒否します。有効性のチェックは次のとおりです。
bool isBase64(String s) {
try {
base64Decode(s);
return true;
} on FormatException {
return false;
}
}
文字列が失敗し、メッセージだけでは原因がわからない場合は、Base64 バリデーター が問題箇所を示します。原因については よくある Base64 のエラー で解説しています。
落とし穴
codeUnitsをエンコードする。base64Encode('é'.codeUnits)は6Q==(UTF-8 ではなく Latin-1)を返し、'✓'.codeUnitsはArgumentError: Invalid argument (Not a byte value at index 0: 0x2713)をスローします。必ずutf8.encodeを通してください。- 他のシステムからの改行。 メールや Linux の
base64による MIME 出力には\nが含まれます。先にs.replaceAll(RegExp(r'\s'), '')で空白を取り除いてください。 base64Encodeが URLセーフだと思い込む。 出力するのは+と/です。URL に入れるものにはbase64Url.encodeを使ってください。- MIME の折り返しがない。 コーデックは改行を挿入しません。必要なら自分で 76 文字ごとの行に分割してください。
よくある質問
Flutter で Base64 の画像を表示するには?
バイト列にデコードして Image.memory(base64Decode(b64)) を使います。文字列が Data URI なら、先にカンマまでを取り除くか、UriData.parse(uri).contentAsBytes() を使ってください。
base64Decode が「Invalid length, must be multiple of four」になるのはなぜですか?
入力にパディングがないためで、JWT や URLセーフなトークンではよくあることです。先に base64.normalize(s) か base64Url.normalize(s) を呼べば、不足している = が補われます。
Dart の base64Decode は URLセーフな入力を受け付けますか?
はい。デコーダーは +/ と -_ の両方を受け付けるので、base64Decode('PDw_Pz8-Pg==') も動きます。違うのはエンコーダーだけで、base64 は +/ を、base64Url は -_ を出力します。