C# で Bitmap 画像と Base64 文字列を相互変換する方法を記載します。
画像を JSON に載せて Web API に送りたい、設定ファイルやデータベースの文字列カラムに画像を埋め込みたい——そんなとき、画像をテキストとして扱える Base64 変換が役に立ちます。私の場合は、画面キャプチャした画像を OCR の Web API に送るときにこの変換を使いました。
結論のコード
Bitmap(画像)→ string(文字列)
using System.Drawing;
using System.IO;
public static string EncodeBitmap(Bitmap bitmap)
{
using (MemoryStream ms = new MemoryStream())
{
bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
byte[] bytes = ms.ToArray();
return Convert.ToBase64String(bytes);
}
}
string(文字列)→ Bitmap(画像)
public static Bitmap DecodeBitmap(string str)
{
byte[] bytes = Convert.FromBase64String(str);
using (MemoryStream ms = new MemoryStream(bytes))
using (Bitmap tmp = new Bitmap(ms))
{
// ストリームから切り離した複製を返す(理由は後述のハマりポイント参照)
return new Bitmap(tmp);
}
}
呼び出し側はこうなります。
// 画像 → Base64 文字列
string base64 = EncodeBitmap(bmp);
// Base64 文字列 → 画像
using (Bitmap restored = DecodeBitmap(base64))
{
restored.Save(@"C:\temp\restored.png", System.Drawing.Imaging.ImageFormat.Png);
}
コードの解説
エンコード側でやっていることは 3 ステップです。
MemoryStreamを用意し、bitmap.Saveで PNG 形式のバイト列としてメモリ上に書き出すms.ToArray()でバイト配列を取り出すConvert.ToBase64Stringでバイト配列を文字列化する
デコード側はその逆で、Convert.FromBase64String で文字列をバイト配列に戻し、MemoryStream 経由で Bitmap を作ります。
保存形式に ImageFormat.Png を選んでいるのは、可逆圧縮で画質が劣化しないためです。写真のようにサイズを抑えたい場合は ImageFormat.Jpeg に変えれば文字列も小さくなりますが、変換のたびに画質が落ちる点には注意してください。
ハマりポイント:「A generic error occurred in GDI+」
デコード処理で一番ハマりやすいのが、復元した Bitmap を使おうとした瞬間に A generic error occurred in GDI+ という例外が出るという現象です。デコード直後は問題なく見えるのに、Save や描画のタイミングで突然失敗します。
原因は、new Bitmap(ms) で作った Bitmap が元のストリームを内部で参照し続けているためです。GDI+ の仕様で、ストリームから作った Bitmap はその Bitmap を使い終わるまでストリームを開いたままにしておく必要があります。using でストリームを閉じた後に Bitmap を使うと、参照先を失った Bitmap が例外を投げるわけです。
対処は、上のコードのように new Bitmap(tmp) でストリームから切り離した複製を作って返すことです。複製はピクセルデータを自分で持つため、ストリームを閉じても安全に使えます。
// NG: ストリームを閉じた後に bmp を使うと GDI+ エラー
using (MemoryStream ms = new MemoryStream(bytes))
{
return new Bitmap(ms);
}
// OK: 複製を返せばストリームを閉じても安全
using (MemoryStream ms = new MemoryStream(bytes))
using (Bitmap tmp = new Bitmap(ms))
{
return new Bitmap(tmp);
}
「デコード直後は動くのに、あとで画像を保存すると落ちる」というときは、まずこのストリームの寿命を疑ってみてください。
data URI 形式(プレフィックス付き)の扱い
Web 系の API や HTML とやり取りする場合、Base64 文字列の先頭に data:image/png;base64, というプレフィックスが付いた「data URI 形式」で受け渡しすることがよくあります。
この形式の文字列をそのまま Convert.FromBase64String に渡すと FormatException になります。デコード前にプレフィックスを取り除いてください。
string base64 = dataUri.Substring(dataUri.IndexOf(",") + 1);
逆に、変換した画像を HTML の <img> タグで直接表示したいときは、プレフィックスを付けて埋め込みます。
<img src="data:image/png;base64,iVBORw0KGgo..." />
画像ファイルを別途用意せずに HTML 1 枚で完結できるので、ツールが出力するレポート HTML などに便利です。
知っておきたい注意点
- サイズが約 1.33 倍になる:Base64 は 3 バイトを 4 文字で表すため、元の画像より約 33% 大きくなります。大きな画像を大量に扱う用途では、素直にバイナリのまま送る設計も検討してください
- 不正な文字列は
FormatException:外部から受け取った文字列をデコードする場合はtry-catchで囲むか、事前にConvert.TryFromBase64Stringで検証すると安全です
まとめ
- 画像 → 文字列は
MemoryStreamにSaveしてConvert.ToBase64String - 文字列 → 画像は
Convert.FromBase64StringからMemoryStream経由でBitmapを作る - ストリームから作った Bitmap は複製を返す(
A generic error occurred in GDI+対策・最重要) - data URI 形式はプレフィックス
data:image/png;base64,の付け外しが必要 - サイズは約 1.33 倍になるので大量の画像には不向き
変換自体は数行ですが、GDI+ のエラーは出るタイミングが遅れて原因を追いにくい、嫌なタイプのバグです。同じところで詰まった方の参考になれば幸いです。