未来の自分へ知識貯金

ゆーきのエンジニアブログ

約4分 C#Visual Studioプログラミング

【C#】Base64画像と文字列の相互変換

【C#】Base64画像と文字列の相互変換

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 ステップです。

  1. MemoryStream を用意し、bitmap.Save で PNG 形式のバイト列としてメモリ上に書き出す
  2. ms.ToArray() でバイト配列を取り出す
  3. 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 で検証すると安全です

まとめ

  • 画像 → 文字列は MemoryStreamSave して Convert.ToBase64String
  • 文字列 → 画像は Convert.FromBase64String から MemoryStream 経由で Bitmap を作る
  • ストリームから作った Bitmap は複製を返すA generic error occurred in GDI+ 対策・最重要)
  • data URI 形式はプレフィックス data:image/png;base64, の付け外しが必要
  • サイズは約 1.33 倍になるので大量の画像には不向き

変換自体は数行ですが、GDI+ のエラーは出るタイミングが遅れて原因を追いにくい、嫌なタイプのバグです。同じところで詰まった方の参考になれば幸いです。

この記事をシェアする

ゆーき
ゆーき

プログラマー経験10年以上。好きなプログラム言語はC#。使える言語はC++, Python他。好きな開発環境はVisual Studio。

プロフィールを見る →