C# Bitmapの使い方完全ガイド|画像の読み込み・保存・描画・リサイズまで解説

はじめに

C#で画像を扱うときによく使われるのが、System.Drawing.Bitmapクラスです。C# Bitmapを使うと、画像ファイルの読み込み、保存、描画、リサイズ、トリミング、ピクセル単位の加工などを比較的シンプルなコードで実装できます。

たとえば、次のような処理に向いています。

  • 画像ファイルを読み込んで別形式で保存する

  • 画像に文字や図形を描画する

  • サムネイル画像を作成する

  • 複数の画像を1枚に合成する

  • ピクセルの色を取得・変更する

  • Windowsフォームで画像を表示する

一方で、C# Bitmapは便利な反面、Dispose忘れによるファイルロック、A generic error occurred in GDI+、.NET 6以降の非Windows環境での制限など、つまずきやすいポイントもあります。

この記事では、C# Bitmapの基本から実用的な画像処理サンプル、よくあるエラーの原因と対策までをまとめて解説します。

1. C# Bitmapとは?できることと利用シーン

1-1. Bitmapクラスの役割とSystem.Drawingとの関係

C#のBitmapクラスは、System.Drawing名前空間に含まれる画像処理用のクラスです。Microsoftのドキュメントでは、Bitmapは「グラフィックスイメージとその属性のピクセルデータで構成されるGDI+ビットマップをカプセル化するクラス」と説明されています。つまり、画像をピクセルの集合として扱い、読み込み・保存・描画・加工を行うためのオブジェクトです。

基本的な使い方は次のとおりです。

C#
using System.Drawing;

using Bitmap bitmap = new Bitmap("sample.jpg");

Console.WriteLine(bitmap.Width);
Console.WriteLine(bitmap.Height);

BitmapImageクラスを継承しているため、画像として共通する機能を持ちながら、ピクセル操作や画像生成などに使いやすいクラスになっています。

1-2. Bitmapで扱える画像形式と主な用途

C# Bitmapでは、BMP、GIF、EXIF、JPG、PNG、TIFFなどの画像形式を扱えます。Microsoftのドキュメントでも、GDI+がこれらの形式をサポートしていることが示されています。

主な用途は次のとおりです。

用途内容
画像変換JPEGをPNGに変換する、BMPをJPEGに変換する
画像加工文字入れ、図形描画、透かし追加
リサイズサムネイル作成、アップロード前の縮小
トリミング指定範囲の切り抜き、正方形サムネイル作成
画面表示WindowsフォームのPictureBoxに表示
ピクセル処理グレースケール化、色変更、透明度処理

PNGは透過を扱いやすく、JPEGは写真の圧縮保存に向いています。BMPは非圧縮に近い形式として扱いやすい一方、ファイルサイズが大きくなりやすいため、Web配信にはあまり向きません。JPEGは写真向きですが、透過やアニメーションには対応していません。PNGはアルファ値を保持できるため、透明背景の画像に向いています。

1-3. Image・Graphics・PictureBoxとの違い

C# Bitmapを理解するには、ImageGraphicsPictureBoxとの違いを押さえておくと便利です。

Imageは画像全般を表す基底クラスです。BitmapImageを継承した具体的な画像クラスで、ピクセル単位の処理や画像生成に使われます。

Graphicsは、画像や画面に描画するためのクラスです。線、四角形、円、文字、別画像などを描くときに使います。Bitmapそのものは画像データを表し、Graphicsはその画像に描くためのペンのような役割です。

PictureBoxはWindowsフォームのUIコントロールです。画像処理をするクラスではなく、画像を画面に表示するために使います。

C#
using System.Drawing;
using System.Windows.Forms;

Bitmap bitmap = new Bitmap("sample.png");

PictureBox pictureBox = new PictureBox();
pictureBox.Image = bitmap;

整理すると、次のようになります。

クラス役割
Image画像全般を表す基底クラス
Bitmapピクセルデータを持つ画像クラス
GraphicsBitmapや画面に描画するためのクラス
PictureBoxWindowsフォームで画像を表示するUI部品

1-4. .NET Framework/.NET 6以降での注意点

.NET FrameworkのWindowsアプリでは、System.Drawingは長く使われてきた画像処理APIです。一方、.NET 6以降ではSystem.Drawing.CommonがWindows専用ライブラリとして扱われるようになりました。非Windows OS向けにコンパイルすると警告が出たり、実行時にPlatformNotSupportedExceptionが発生したりする場合があります。

そのため、.NET 6以降でC# Bitmapを使う場合は、基本的にWindows向けアプリで使うものと考えるのが安全です。

XML
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>

WindowsフォームやWPFなど、Windowsデスクトップアプリで使う場合は問題になりにくいですが、LinuxサーバーやDockerコンテナ上で画像処理を行うWebアプリでは注意が必要です。

1-5. Windows以外の環境で使う場合の制限と代替ライブラリ

Mac、Linux、LinuxベースのDockerコンテナなどで画像処理を行う場合、System.Drawing.Commonに依存したC# Bitmap処理は避けたほうが安全です。Microsoftは、クロスプラットフォームアプリでこれらのAPIを使う場合の移行先として、SkiaSharp、ImageSharp、Aspose.Drawing、Microsoft.Maui.Graphicsなどを挙げています。

代表的な代替ライブラリは次のとおりです。

ライブラリ特徴
ImageSharpC#だけで書かれた画像処理ライブラリ。Webアプリやクロスプラットフォーム用途で使いやすい
SkiaSharpGoogle Skiaベース。高速な描画やクロスプラットフォーム用途に向く
Aspose.Drawing商用ライブラリ。System.Drawing互換を意識したAPI
Microsoft.Maui.GraphicsMAUI系アプリやクロスプラットフォーム描画向け

WindowsデスクトップアプリならC# Bitmap、クロスプラットフォームやサーバーサイド画像処理ならImageSharpやSkiaSharpを検討するとよいでしょう。

2. C#でBitmapを使うための準備

2-1. 必要な名前空間と参照設定

C# Bitmapを使うには、基本的に次の名前空間を使用します。

C#
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;

それぞれの用途は次のとおりです。

名前空間用途
System.DrawingBitmap、Image、Graphics、Color、Fontなど
System.Drawing.ImagingImageFormat、ImageCodecInfo、EncoderParameters、PixelFormatなど
System.Drawing.Drawing2DInterpolationMode、SmoothingMode、GraphicsPathなど

画像を読み込むだけならSystem.Drawingで十分ですが、JPEG画質指定、PixelFormat、LockBits、高画質リサイズなどを使う場合はSystem.Drawing.ImagingSystem.Drawing.Drawing2Dも必要になります。

2-2. System.Drawing.Commonのインストール方法

.NET 6以降のコンソールアプリやクラスライブラリでC# Bitmapを使う場合は、NuGetからSystem.Drawing.Commonを追加します。

Bash
dotnet add package System.Drawing.Common

Visual Studioの場合は、次の手順で追加できます。

  1. プロジェクトを右クリック

  2. 「NuGet パッケージの管理」を選択

  3. System.Drawing.Commonを検索

  4. インストール

.csprojに直接書く場合は、次のようになります。

XML
<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>

バージョンはプロジェクトの.NETバージョンに合わせて選びます。Windows専用として使う場合は、ターゲットフレームワークに-windowsを付けておくと意図が明確になります。

XML
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>

2-3. Visual Studioでのプロジェクト設定

WindowsフォームアプリでC# Bitmapを使う場合は、通常、テンプレート作成時点で必要な設定が含まれています。

Windowsフォームの場合の.csproj例です。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

</Project>

WPFの場合は次のようになります。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>

</Project>

コンソールアプリで画像処理だけを行う場合も、Windows専用であれば次のように設定できます。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup>

</Project>

2-4. コンソールアプリ・Windowsフォーム・WPFでの使い分け

C# Bitmapは、プロジェクトの種類によって使い方が少し変わります。

コンソールアプリでは、画像を読み込んで保存するようなバッチ処理に向いています。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");
bitmap.Save("output.png", ImageFormat.Png);

Windowsフォームでは、PictureBoxと組み合わせて画像を表示・編集する用途に向いています。

C#
pictureBox1.Image = new Bitmap("sample.jpg");

WPFでは、UI側の標準画像型がBitmapSourceBitmapImageなので、System.Drawing.Bitmapをそのまま表示するより、WPFの画像型に変換する設計が必要です。

C# Bitmapを選ぶ基準は次のとおりです。

アプリ種別Bitmapの使いどころ
コンソールアプリ一括変換、リサイズ、トリミング
Windowsフォーム画像表示、簡易編集ツール、描画アプリ
WPF画像処理部分だけBitmapを使い、表示はBitmapSourceに変換
ASP.NET / Web API原則としてImageSharpやSkiaSharpを検討

2-5. Bitmapを使う前に知っておきたいDisposeの重要性

C# Bitmapで最も重要なポイントのひとつがDisposeです。BitmapGraphicsPenBrushFontなどはアンマネージリソースを内部で使用するため、使い終わったら確実に破棄する必要があります。

基本はusing文を使います。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");

// bitmapを使った処理
Console.WriteLine(bitmap.Width);

古い書き方では次のようになります。

C#
using (Bitmap bitmap = new Bitmap("sample.jpg"))
{
Console.WriteLine(bitmap.Width);
}

Disposeを忘れると、次のような問題が起こります。

  • 画像ファイルがロックされて削除できない

  • 同じファイルに上書き保存できない

  • メモリ使用量が増え続ける

  • GDI+関連のエラーが発生する

  • 大量画像処理でアプリが不安定になる

C# Bitmapを安全に使うには、「作ったら破棄する」を徹底しましょう。

3. Bitmapで画像を読み込む方法

3-1. ファイルパスから画像を読み込む基本コード

最も基本的な読み込み方法は、ファイルパスを指定してBitmapを生成する方法です。

C#
using System.Drawing;

using Bitmap bitmap = new Bitmap("sample.jpg");

Console.WriteLine($"幅: {bitmap.Width}");
Console.WriteLine($"高さ: {bitmap.Height}");

絶対パスを指定する場合は、エスケープを避けるために逐語的文字列リテラルを使うと便利です。

C#
using Bitmap bitmap = new Bitmap(@"C:\Images\sample.jpg");

相対パスを使う場合は、実行ファイルのカレントディレクトリを基準に解決されます。

C#
using Bitmap bitmap = new Bitmap("images/sample.jpg");

ファイルが存在しない場合や、画像として読み込めない場合は例外が発生するため、実用コードでは例外処理を入れます。

3-2. StreamからBitmapを作成する方法

ファイルだけでなく、StreamからBitmapを作成することもできます。

C#
using System.Drawing;

using FileStream stream = new FileStream("sample.png", FileMode.Open, FileAccess.Read);
using Bitmap bitmap = new Bitmap(stream);

Console.WriteLine(bitmap.Width);
Console.WriteLine(bitmap.Height);

Webから受け取った画像、データベースに保存された画像、メモリ上のバイト配列などを扱う場合に便利です。

バイト配列から作成する場合は、MemoryStreamを使います。

C#
byte[] imageBytes = File.ReadAllBytes("sample.png");

using MemoryStream ms = new MemoryStream(imageBytes);
using Bitmap bitmap = new Bitmap(ms);

Console.WriteLine(bitmap.Size);

注意点として、Bitmapは内部的に元のストリームに依存する場合があります。ストリームをすぐ閉じたい場合は、いったん別のBitmapにコピーして使うと安全です。

C#
Bitmap LoadBitmapFromBytes(byte[] bytes)
{
using MemoryStream ms = new MemoryStream(bytes);
using Bitmap temp = new Bitmap(ms);
return new Bitmap(temp);
}

呼び出し側では、返されたBitmapを必ず破棄します。

C#
using Bitmap bitmap = LoadBitmapFromBytes(File.ReadAllBytes("sample.jpg"));

3-3. ファイルロックを避けて画像を読み込む方法

new Bitmap("sample.jpg")で画像を読み込むと、Bitmapを破棄するまで元ファイルがロックされることがあります。そのため、読み込み後に元ファイルを削除・上書きしたい場合は、ファイルロックを避ける読み込み方を使います。

C#
using System.Drawing;

Bitmap LoadBitmapWithoutLock(string path)
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using Bitmap temp = new Bitmap(fs);
return new Bitmap(temp);
}

使い方は次のとおりです。

C#
using Bitmap bitmap = LoadBitmapWithoutLock("sample.jpg");

// この時点で元ファイルを削除・移動しやすい
File.Delete("sample.jpg");

ポイントは、ストリームから読み込んだBitmapをそのまま返すのではなく、new Bitmap(temp)でコピーを作って返すことです。これにより、元ファイルや元ストリームへの依存を切り離しやすくなります。

3-4. 存在しないファイルや非対応形式の例外処理

実用的なコードでは、ファイルの存在確認と例外処理を入れます。

C#
using System.Drawing;

string path = "sample.jpg";

if (!File.Exists(path))
{
Console.WriteLine("ファイルが存在しません。");
return;
}

try
{
using Bitmap bitmap = new Bitmap(path);
Console.WriteLine($"読み込み成功: {bitmap.Width} x {bitmap.Height}");
}
catch (ArgumentException)
{
Console.WriteLine("画像形式が非対応、または画像ファイルが破損している可能性があります。");
}
catch (OutOfMemoryException)
{
Console.WriteLine("画像が大きすぎる、または画像として読み込めない可能性があります。");
}
catch (Exception ex)
{
Console.WriteLine($"予期しないエラー: {ex.Message}");
}

OutOfMemoryExceptionは本当にメモリが足りない場合だけでなく、画像として解釈できないファイルを読み込んだ場合にも発生することがあります。ユーザーがアップロードしたファイルを処理する場合は、拡張子だけで判断せず、実際に読み込めるかどうかを確認しましょう。

3-5. 画像サイズ・幅・高さを取得する方法

画像のサイズは、WidthHeightSizeプロパティで取得できます。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");

int width = bitmap.Width;
int height = bitmap.Height;
Size size = bitmap.Size;

Console.WriteLine($"幅: {width}");
Console.WriteLine($"高さ: {height}");
Console.WriteLine($"サイズ: {size}");

画像の縦横比を求める場合は、次のように計算します。

C#
double aspectRatio = (double)bitmap.Width / bitmap.Height;

Console.WriteLine($"アスペクト比: {aspectRatio}");

リサイズやトリミングを行う前には、必ず元画像の幅と高さを確認しておくと、範囲外エラーを防ぎやすくなります。

4. Bitmapで画像を保存する方法

4-1. Saveメソッドの基本的な使い方

Bitmap画像を保存するには、Saveメソッドを使います。Bitmapはファイル、ストリームなどから画像を作成でき、Saveメソッドでファイルシステムやストリームに保存できます。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");

bitmap.Save("output.png", ImageFormat.Png);

保存形式はImageFormatで指定します。

C#
bitmap.Save("output.jpg", ImageFormat.Jpeg);
bitmap.Save("output.png", ImageFormat.Png);
bitmap.Save("output.bmp", ImageFormat.Bmp);
bitmap.Save("output.gif", ImageFormat.Gif);

拡張子とImageFormatは必ず一致させましょう。たとえば、ファイル名が.jpgなのにImageFormat.Pngで保存すると、拡張子と実際の画像形式が一致しないファイルになります。

4-2. PNG・JPEG・BMP・GIF形式で保存する方法

形式ごとの保存例です。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");

bitmap.Save("output.png", ImageFormat.Png);
bitmap.Save("output.jpg", ImageFormat.Jpeg);
bitmap.Save("output.bmp", ImageFormat.Bmp);
bitmap.Save("output.gif", ImageFormat.Gif);

形式ごとの特徴は次のとおりです。

形式特徴
PNG劣化なし。透過に対応。ロゴ、UI画像、スクリーンショット向き
JPEG写真向き。非可逆圧縮。ファイルサイズを小さくしやすい
BMPWindows標準のビットマップ形式。容量が大きくなりやすい
GIF256色まで。簡単な図形やアニメーション向き

写真を保存するならJPEG、透過が必要ならPNG、加工途中の一時保存ならPNGやBMPを選ぶとよいでしょう。

4-3. JPEGの画質を指定して保存する方法

JPEGの画質を指定して保存するには、ImageCodecInfoEncoderParametersを使います。

C#
using System.Drawing;
using System.Drawing.Imaging;

static ImageCodecInfo GetEncoder(ImageFormat format)
{
return ImageCodecInfo.GetImageEncoders()
.First(codec => codec.FormatID == format.Guid);
}

using Bitmap bitmap = new Bitmap("input.jpg");

ImageCodecInfo jpegCodec = GetEncoder(ImageFormat.Jpeg);

using EncoderParameters encoderParams = new EncoderParameters(1);
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 85L);

bitmap.Save("output-quality85.jpg", jpegCodec, encoderParams);

Encoder.Qualityは一般的に0〜100の範囲で指定します。数値が大きいほど高画質ですが、ファイルサイズも大きくなります。

目安は次のとおりです。

画質用途
50〜70ファイルサイズ重視
80〜90Web掲載向けの標準的な画質
95以上高画質重視。ただし容量が大きくなりやすい

Webサイト用の画像では、画質80〜85程度から試すと、見た目と容量のバランスを取りやすいです。

4-4. 上書き保存でエラーになる原因と対処法

C# Bitmapでよくある失敗が、読み込んだ元画像と同じパスに上書き保存することです。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");

// これはエラーになることがある
bitmap.Save("sample.jpg", ImageFormat.Jpeg);

元ファイルをBitmapが保持している状態で同じファイルに保存しようとすると、ファイルロックやGDI+エラーの原因になります。

安全な方法は、別名で保存してから置き換えることです。

C#
string inputPath = "sample.jpg";
string tempPath = "sample_temp.jpg";

using (Bitmap bitmap = new Bitmap(inputPath))
{
bitmap.Save(tempPath, ImageFormat.Jpeg);
}

// 必要なら元ファイルを置き換える
File.Delete(inputPath);
File.Move(tempPath, inputPath);

さらに安全にするなら、ファイルロックを避けて読み込んでから保存します。

C#
Bitmap LoadBitmapWithoutLock(string path)
{
using FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using Bitmap temp = new Bitmap(fs);
return new Bitmap(temp);
}

using Bitmap bitmap = LoadBitmapWithoutLock("sample.jpg");
bitmap.Save("sample_temp.jpg", ImageFormat.Jpeg);

4-5. 保存時によくあるGDI+エラーの解決方法

保存時に次のようなエラーが出ることがあります。

A generic error occurred in GDI+.

原因はさまざまですが、代表的には次のようなケースです。

原因対策
保存先フォルダが存在しないDirectory.CreateDirectoryで事前に作成する
保存先に書き込み権限がない権限のある場所に保存する
元画像と同じパスに保存している一時ファイルに保存して置き換える
BitmapやStreamが破棄されているusingの範囲を見直す
保存形式と拡張子が不一致.jpgならImageFormat.Jpegを指定する
画像がインデックスカラー形式24bppや32bppのBitmapに変換して保存する

保存先フォルダを作る例です。

C#
string outputDir = @"C:\Output";
Directory.CreateDirectory(outputDir);

using Bitmap bitmap = new Bitmap("input.png");
bitmap.Save(Path.Combine(outputDir, "output.png"), ImageFormat.Png);

画像形式を変換してから保存する例です。

C#
using Bitmap source = new Bitmap("input.gif");
using Bitmap converted = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);

using (Graphics g = Graphics.FromImage(converted))
{
g.DrawImage(source, 0, 0, source.Width, source.Height);
}

converted.Save("output.jpg", ImageFormat.Jpeg);

5. Bitmapに画像や図形を描画する方法

5-1. Graphics.FromImageで描画する基本手順

Bitmapに線や文字、別画像を描くには、Graphics.FromImageを使ってGraphicsオブジェクトを作成します。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap(400, 300);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.DrawLine(Pens.Red, 10, 10, 390, 290);
}

bitmap.Save("draw-line.png", ImageFormat.Png);

基本手順は次のとおりです。

  1. 描画先のBitmapを作成する

  2. Graphics.FromImage(bitmap)でGraphicsを取得する

  3. DrawLineDrawRectangleDrawStringなどで描画する

  4. Bitmapを保存する

  5. BitmapとGraphicsを破棄する

Graphicsもリソースを持つため、必ずusingで破棄しましょう。

5-2. 線・四角形・円を描画する方法

線、四角形、円を描画するサンプルです。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap(500, 400);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);

using Pen redPen = new Pen(Color.Red, 3);
using Pen bluePen = new Pen(Color.Blue, 3);
using Pen greenPen = new Pen(Color.Green, 3);

g.DrawLine(redPen, 20, 20, 480, 20);
g.DrawRectangle(bluePen, 50, 80, 200, 120);
g.DrawEllipse(greenPen, 280, 80, 150, 150);
}

bitmap.Save("shapes.png", ImageFormat.Png);

塗りつぶし図形を描く場合は、FillRectangleFillEllipseを使います。

C#
using Bitmap bitmap = new Bitmap(500, 400);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);

using Brush orangeBrush = new SolidBrush(Color.Orange);
using Brush skyBrush = new SolidBrush(Color.SkyBlue);

g.FillRectangle(orangeBrush, 50, 50, 200, 100);
g.FillEllipse(skyBrush, 280, 50, 150, 150);
}

bitmap.Save("filled-shapes.png", ImageFormat.Png);

5-3. 文字を画像に書き込む方法

画像に文字を入れるには、DrawStringを使います。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap(800, 450);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);

using Font font = new Font("Meiryo", 36, FontStyle.Bold);
using Brush brush = new SolidBrush(Color.Black);

g.DrawString("C# Bitmap Sample", font, brush, new PointF(50, 180));
}

bitmap.Save("text.png", ImageFormat.Png);

既存画像に文字を重ねる場合は、次のようにします。

C#
using Bitmap bitmap = new Bitmap("input.jpg");

using (Graphics g = Graphics.FromImage(bitmap))
{
using Font font = new Font("Meiryo", 32, FontStyle.Bold);
using Brush brush = new SolidBrush(Color.FromArgb(200, Color.White));

g.DrawString("Sample Text", font, brush, new PointF(30, 30));
}

bitmap.Save("text-on-image.jpg", ImageFormat.Jpeg);

半透明の文字を描きたい場合は、Color.FromArgbでアルファ値を指定します。

C#
using Brush brush = new SolidBrush(Color.FromArgb(180, 255, 255, 255));

アルファ値は0が完全透明、255が不透明です。

5-4. 背景色を塗りつぶす方法

背景色を塗りつぶすには、Graphics.Clearを使うのが簡単です。

C#
using Bitmap bitmap = new Bitmap(600, 400);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.LightGray);
}

bitmap.Save("background.png", ImageFormat.Png);

一部の範囲だけ塗りつぶす場合は、FillRectangleを使います。

C#
using Bitmap bitmap = new Bitmap(600, 400);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);

using Brush brush = new SolidBrush(Color.LightBlue);
g.FillRectangle(brush, 100, 100, 300, 150);
}

bitmap.Save("fill-area.png", ImageFormat.Png);

透過背景のPNGを作る場合は、32bit ARGBのBitmapを作成します。

C#
using Bitmap bitmap = new Bitmap(600, 400, PixelFormat.Format32bppArgb);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);

using Brush brush = new SolidBrush(Color.FromArgb(180, Color.Red));
g.FillEllipse(brush, 100, 80, 300, 200);
}

bitmap.Save("transparent.png", ImageFormat.Png);

5-5. 別の画像をBitmap上に合成する方法

別画像を合成するには、DrawImageを使います。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap background = new Bitmap("background.jpg");
using Bitmap logo = new Bitmap("logo.png");

using (Graphics g = Graphics.FromImage(background))
{
int x = background.Width - logo.Width - 20;
int y = background.Height - logo.Height - 20;

g.DrawImage(logo, x, y, logo.Width, logo.Height);
}

background.Save("merged.jpg", ImageFormat.Jpeg);

ロゴを右下に配置する処理などでよく使います。

半透明で合成したい場合は、ImageAttributesとカラーマトリックスを使います。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap background = new Bitmap("background.jpg");
using Bitmap overlay = new Bitmap("overlay.png");

using ImageAttributes attributes = new ImageAttributes();

ColorMatrix matrix = new ColorMatrix
{
Matrix33 = 0.5f
};

attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

using (Graphics g = Graphics.FromImage(background))
{
Rectangle dest = new Rectangle(50, 50, overlay.Width, overlay.Height);

g.DrawImage(
overlay,
dest,
0,
0,
overlay.Width,
overlay.Height,
GraphicsUnit.Pixel,
attributes);
}

background.Save("overlay-result.jpg", ImageFormat.Jpeg);

5-6. Pen・Brush・Fontを安全に破棄する書き方

PenBrushFontも破棄が必要です。特に大量画像処理や繰り返し描画では、破棄を忘れるとリソース不足の原因になります。

安全な書き方です。

C#
using Bitmap bitmap = new Bitmap(500, 300);

using (Graphics g = Graphics.FromImage(bitmap))
using (Pen pen = new Pen(Color.Red, 3))
using (Brush brush = new SolidBrush(Color.Blue))
using (Font font = new Font("Meiryo", 24))
{
g.Clear(Color.White);
g.DrawRectangle(pen, 20, 20, 200, 100);
g.DrawString("Hello", font, brush, 30, 150);
}

bitmap.Save("safe-draw.png", ImageFormat.Png);

C# 8以降なら、using宣言で簡潔に書くこともできます。

C#
using Bitmap bitmap = new Bitmap(500, 300);
using Graphics g = Graphics.FromImage(bitmap);
using Pen pen = new Pen(Color.Red, 3);
using Brush brush = new SolidBrush(Color.Blue);
using Font font = new Font("Meiryo", 24);

g.Clear(Color.White);
g.DrawRectangle(pen, 20, 20, 200, 100);
g.DrawString("Hello", font, brush, 30, 150);

bitmap.Save("safe-draw.png", ImageFormat.Png);

6. Bitmap画像をリサイズする方法

6-1. 指定した幅・高さに画像を拡大縮小する方法

Bitmap画像を指定サイズにリサイズする基本コードです。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap source = new Bitmap("input.jpg");
using Bitmap resized = new Bitmap(300, 200);

using (Graphics g = Graphics.FromImage(resized))
{
g.DrawImage(source, 0, 0, 300, 200);
}

resized.Save("resized.jpg", ImageFormat.Jpeg);

この方法では、元画像の縦横比に関係なく300×200に変形されます。人物や商品画像では、縦横比が崩れると不自然に見えるため、必要に応じてアスペクト比を維持しましょう。

6-2. アスペクト比を維持してリサイズする方法

アスペクト比を維持して、指定した最大幅・最大高さに収めるサンプルです。

C#
using System.Drawing;
using System.Drawing.Imaging;

static Size CalculateResizeSize(int originalWidth, int originalHeight, int maxWidth, int maxHeight)
{
double ratioX = (double)maxWidth / originalWidth;
double ratioY = (double)maxHeight / originalHeight;
double ratio = Math.Min(ratioX, ratioY);

int newWidth = (int)(originalWidth * ratio);
int newHeight = (int)(originalHeight * ratio);

return new Size(newWidth, newHeight);
}

using Bitmap source = new Bitmap("input.jpg");

Size newSize = CalculateResizeSize(source.Width, source.Height, 800, 600);

using Bitmap resized = new Bitmap(newSize.Width, newSize.Height);

using (Graphics g = Graphics.FromImage(resized))
{
g.DrawImage(source, 0, 0, newSize.Width, newSize.Height);
}

resized.Save("resized-keep-aspect.jpg", ImageFormat.Jpeg);

このコードでは、元画像を800×600以内に収めつつ、縦横比を維持します。

たとえば、元画像が1600×900なら、800×450になります。元画像が900×1600なら、337×600になります。

6-3. 高画質にリサイズするためのInterpolationMode設定

リサイズ時の画質を上げるには、Graphicsの描画品質設定を行います。

C#
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

using Bitmap source = new Bitmap("input.jpg");
using Bitmap resized = new Bitmap(800, 600);

using (Graphics g = Graphics.FromImage(resized))
{
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

g.DrawImage(source, 0, 0, 800, 600);
}

resized.Save("resized-high-quality.jpg", ImageFormat.Jpeg);

特に重要なのはInterpolationMode.HighQualityBicubicです。単純な拡大縮小よりも、なめらかな結果になりやすくなります。

ただし、高画質設定にすると処理時間は増えるため、大量画像の一括処理では速度とのバランスを見て調整します。

6-4. サムネイル画像を作成する方法

サムネイル画像を作成する場合は、アスペクト比を維持して小さくするのが一般的です。

C#
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

static Bitmap CreateThumbnail(Bitmap source, int maxWidth, int maxHeight)
{
double ratioX = (double)maxWidth / source.Width;
double ratioY = (double)maxHeight / source.Height;
double ratio = Math.Min(ratioX, ratioY);

int width = (int)(source.Width * ratio);
int height = (int)(source.Height * ratio);

Bitmap thumbnail = new Bitmap(width, height);

using Graphics g = Graphics.FromImage(thumbnail);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

g.DrawImage(source, 0, 0, width, height);

return thumbnail;
}

using Bitmap source = new Bitmap("input.jpg");
using Bitmap thumbnail = CreateThumbnail(source, 300, 300);

thumbnail.Save("thumbnail.jpg", ImageFormat.Jpeg);

正方形のサムネイルを作りたい場合は、リサイズだけでなくトリミングも組み合わせる必要があります。

6-5. リサイズ時に画像が荒くなる原因と対策

リサイズ後の画像が荒くなる主な原因は次のとおりです。

原因対策
低品質な補間で縮小しているHighQualityBicubicを指定する
小さい画像を大きく拡大している元画像より大きくしすぎない
JPEG画質が低すぎる画質80〜90程度で保存する
何度もリサイズを繰り返している常に元画像からリサイズする
縦横比を無視しているアスペクト比を維持する

特に、画像を「縮小して保存したものをさらに縮小する」と、劣化が重なります。できるだけ元画像から目的サイズを直接生成しましょう。

7. Bitmapで画像をトリミング・切り抜きする方法

7-1. Cloneメソッドで指定範囲を切り抜く方法

Bitmapの指定範囲を切り抜くには、Cloneメソッドを使えます。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap source = new Bitmap("input.jpg");

Rectangle cropArea = new Rectangle(100, 50, 300, 200);

using Bitmap cropped = source.Clone(cropArea, source.PixelFormat);

cropped.Save("cropped.png", ImageFormat.Png);

Rectangleには、切り抜き開始位置のX座標、Y座標、幅、高さを指定します。

C#
new Rectangle(x, y, width, height)

ただし、指定範囲が元画像の外にはみ出すとエラーになります。実用コードでは、事前に範囲チェックを行いましょう。

7-2. Graphics.DrawImageでトリミングする方法

Graphics.DrawImageを使って、指定範囲を新しいBitmapに描画する方法もあります。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap source = new Bitmap("input.jpg");

Rectangle sourceRect = new Rectangle(100, 50, 300, 200);
Rectangle destRect = new Rectangle(0, 0, 300, 200);

using Bitmap cropped = new Bitmap(300, 200);

using (Graphics g = Graphics.FromImage(cropped))
{
g.DrawImage(source, destRect, sourceRect, GraphicsUnit.Pixel);
}

cropped.Save("cropped-drawimage.png", ImageFormat.Png);

この方法は、切り抜きと同時にリサイズしたい場合にも使いやすいです。

7-3. 中央基準で正方形に切り抜く方法

SNSアイコンや商品サムネイルでは、画像の中央を基準に正方形へ切り抜くことがよくあります。

C#
using System.Drawing;
using System.Drawing.Imaging;

static Bitmap CropCenterSquare(Bitmap source)
{
int side = Math.Min(source.Width, source.Height);

int x = (source.Width - side) / 2;
int y = (source.Height - side) / 2;

Rectangle cropArea = new Rectangle(x, y, side, side);

return source.Clone(cropArea, source.PixelFormat);
}

using Bitmap source = new Bitmap("input.jpg");
using Bitmap square = CropCenterSquare(source);

square.Save("square.png", ImageFormat.Png);

横長画像なら左右をカットし、縦長画像なら上下をカットして、中央の正方形部分を取り出します。

7-4. リサイズとトリミングを同時に行う方法

中央正方形に切り抜き、指定サイズにリサイズするサンプルです。

C#
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

static Bitmap CreateSquareThumbnail(Bitmap source, int size)
{
int side = Math.Min(source.Width, source.Height);

int x = (source.Width - side) / 2;
int y = (source.Height - side) / 2;

Rectangle sourceRect = new Rectangle(x, y, side, side);
Rectangle destRect = new Rectangle(0, 0, size, size);

Bitmap thumbnail = new Bitmap(size, size, PixelFormat.Format24bppRgb);

using Graphics g = Graphics.FromImage(thumbnail);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

g.DrawImage(source, destRect, sourceRect, GraphicsUnit.Pixel);

return thumbnail;
}

using Bitmap source = new Bitmap("input.jpg");
using Bitmap thumbnail = CreateSquareThumbnail(source, 300);

thumbnail.Save("square-thumbnail.jpg", ImageFormat.Jpeg);

この方法なら、正方形トリミングとリサイズを1回の描画で行えます。

7-5. 範囲外指定で発生するエラーの防ぎ方

トリミング範囲が画像の外側にはみ出すと、例外が発生します。安全に切り抜くには、範囲が画像内に収まっているか確認します。

C#
static Rectangle ClampRectangle(Rectangle rect, int maxWidth, int maxHeight)
{
int x = Math.Max(0, rect.X);
int y = Math.Max(0, rect.Y);

int width = Math.Min(rect.Width, maxWidth - x);
int height = Math.Min(rect.Height, maxHeight - y);

return new Rectangle(x, y, width, height);
}

使い方です。

C#
using Bitmap source = new Bitmap("input.jpg");

Rectangle requested = new Rectangle(100, 100, 1000, 1000);
Rectangle safeRect = ClampRectangle(requested, source.Width, source.Height);

using Bitmap cropped = source.Clone(safeRect, source.PixelFormat);
cropped.Save("safe-cropped.png", ImageFormat.Png);

ユーザー入力でトリミング範囲を指定する場合は、必ず範囲チェックを入れましょう。

8. Bitmapのピクセル操作

8-1. GetPixel・SetPixelで色を取得・変更する方法

C# Bitmapでは、GetPixelで指定座標の色を取得し、SetPixelで色を変更できます。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.png");

Color color = bitmap.GetPixel(10, 20);
Console.WriteLine($"R={color.R}, G={color.G}, B={color.B}, A={color.A}");

bitmap.SetPixel(10, 20, Color.Red);

bitmap.Save("setpixel.png", ImageFormat.Png);

小さな画像や、数ピクセルだけ変更する処理なら簡単で便利です。

指定範囲を赤く塗る例です。

C#
using Bitmap bitmap = new Bitmap("input.png");

for (int y = 50; y < 100; y++)
{
for (int x = 50; x < 100; x++)
{
bitmap.SetPixel(x, y, Color.Red);
}
}

bitmap.Save("red-area.png", ImageFormat.Png);

ただし、大量のピクセルを処理する場合、GetPixelSetPixelは非常に遅くなりやすいです。高速化したい場合はLockBitsを使います。

8-2. 画像をグレースケール化する方法

GetPixelSetPixelでグレースケール化する基本例です。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");

for (int y = 0; y < bitmap.Height; y++)
{
for (int x = 0; x < bitmap.Width; x++)
{
Color original = bitmap.GetPixel(x, y);

int gray = (int)(original.R * 0.299 + original.G * 0.587 + original.B * 0.114);

Color grayColor = Color.FromArgb(original.A, gray, gray, gray);
bitmap.SetPixel(x, y, grayColor);
}
}

bitmap.Save("grayscale.jpg", ImageFormat.Jpeg);

RGBの単純平均でもグレースケール化できます。

C#
int gray = (original.R + original.G + original.B) / 3;

ただし、人間の目は緑に敏感で青に鈍いため、一般的には0.299R + 0.587G + 0.114Bのような重み付き計算のほうが自然に見えます。

8-3. 透過色・アルファ値を扱う方法

透過を扱うには、アルファ値を持つPixelFormatを使います。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap(400, 300, PixelFormat.Format32bppArgb);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);

using Brush brush = new SolidBrush(Color.FromArgb(128, Color.Red));
g.FillEllipse(brush, 50, 50, 300, 200);
}

bitmap.Save("alpha.png", ImageFormat.Png);

Color.FromArgbでは、最初の引数にアルファ値を指定できます。

C#
Color semiTransparentRed = Color.FromArgb(128, 255, 0, 0);

アルファ値の意味は次のとおりです。

アルファ値意味
0完全透明
128半透明
255不透明

透過を保持したい場合は、JPEGではなくPNGで保存しましょう。JPEGは透過に対応していません。

8-4. 大量ピクセル処理が遅い理由

GetPixelSetPixelは便利ですが、1ピクセルごとにメソッド呼び出しが発生します。画像が大きくなると、呼び出し回数が膨大になります。

たとえば、4000×3000ピクセルの画像では、1200万ピクセルあります。

4000 × 3000 = 12,000,000

全ピクセルに対してGetPixelSetPixelを行うと、合計で数千万回のメソッド呼び出しになるため、処理が非常に遅くなります。

小さな画像や試作コードならGetPixelSetPixelで問題ありませんが、本格的な画像処理ではLockBitsを使いましょう。

8-5. LockBitsを使って高速に画像処理する方法

LockBitsを使うと、Bitmapのピクセルデータにまとめてアクセスできます。次は、24bit RGB画像を高速にグレースケール化する例です。

C#
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

static void GrayscaleWithLockBits(Bitmap bitmap)
{
Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);

BitmapData data = bitmap.LockBits(
rect,
ImageLockMode.ReadWrite,
PixelFormat.Format24bppRgb);

try
{
int stride = data.Stride;
int bytes = Math.Abs(stride) * bitmap.Height;
byte[] buffer = new byte[bytes];

Marshal.Copy(data.Scan0, buffer, 0, bytes);

for (int y = 0; y < bitmap.Height; y++)
{
int row = y * stride;

for (int x = 0; x < bitmap.Width; x++)
{
int index = row + x * 3;

byte b = buffer[index];
byte g = buffer[index + 1];
byte r = buffer[index + 2];

byte gray = (byte)(r * 0.299 + g * 0.587 + b * 0.114);

buffer[index] = gray;
buffer[index + 1] = gray;
buffer[index + 2] = gray;
}
}

Marshal.Copy(buffer, 0, data.Scan0, bytes);
}
finally
{
bitmap.UnlockBits(data);
}
}

using Bitmap source = new Bitmap("input.jpg");
using Bitmap bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.DrawImage(source, 0, 0, source.Width, source.Height);
}

GrayscaleWithLockBits(bitmap);

bitmap.Save("grayscale-lockbits.jpg", ImageFormat.Jpeg);

LockBitsでは、PixelFormatによって1ピクセルあたりのバイト数や色の並びが変わります。24bit RGBでは、一般的にB、G、Rの順で3バイトずつ扱います。

高速ですが、インデックス計算を間違えると画像が壊れるため、まずは小さな画像でテストしましょう。

9. Bitmapを画面に表示する方法

9-1. WindowsフォームのPictureBoxに表示する方法

WindowsフォームでBitmapを表示するには、PictureBox.ImageにBitmapを設定します。

C#
using System.Drawing;
using System.Windows.Forms;

public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();

pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox1.Image = new Bitmap("sample.jpg");
}
}

SizeModeを設定すると表示方法を変えられます。

SizeMode内容
Normal左上に原寸表示
StretchImagePictureBoxサイズに引き伸ばし
Zoomアスペクト比を維持して表示
CenterImage中央に表示
AutoSize画像サイズに合わせてPictureBoxを変更

画像ビューアのように自然に表示したい場合は、PictureBoxSizeMode.Zoomがよく使われます。

9-2. PaintイベントでBitmapを描画する方法

PictureBoxを使わず、フォームやパネルのPaintイベントでBitmapを描画することもできます。

C#
private Bitmap? _bitmap;

private void Form1_Load(object sender, EventArgs e)
{
_bitmap = new Bitmap("sample.jpg");
}

private void Form1_Paint(object sender, PaintEventArgs e)
{
if (_bitmap == null) return;

e.Graphics.DrawImage(_bitmap, 0, 0, 400, 300);
}

protected override void OnFormClosed(FormClosedEventArgs e)
{
_bitmap?.Dispose();
base.OnFormClosed(e);
}

Paintイベントでは、e.Graphicsを使って画面に描画します。再描画したい場合は、Invalidate()を呼び出します。

C#
this.Invalidate();

ズーム、スクロール、選択範囲表示などを自前で制御したい場合は、Paintイベントを使う設計が向いています。

9-3. WPFでBitmapを表示する場合の考え方

WPFでは、UIの画像表示にはSystem.Windows.Controls.ImageBitmapSource系の型を使います。System.Drawing.Bitmapを直接表示するより、BitmapImageBitmapSourceに変換して表示するのが一般的です。

MemoryStream経由で変換する例です。

C#
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Media.Imaging;

static BitmapImage ConvertToBitmapImage(Bitmap bitmap)
{
using MemoryStream ms = new MemoryStream();
bitmap.Save(ms, ImageFormat.Png);
ms.Position = 0;

BitmapImage image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = ms;
image.EndInit();
image.Freeze();

return image;
}

使い方です。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");
imageControl.Source = ConvertToBitmapImage(bitmap);

WPF中心のアプリでは、最初からBitmapImageWriteableBitmapを使ったほうが設計しやすいこともあります。C# Bitmapは、画像処理部分だけに限定して使うと扱いやすいです。

9-4. 表示中の画像を差し替えるときのDispose処理

WindowsフォームでPictureBox.Imageを差し替えるときは、古い画像を破棄する必要があります。

悪い例です。

C#
pictureBox1.Image = new Bitmap("new.jpg");

この書き方を繰り返すと、古い画像が破棄されず、ファイルロックやメモリ増加の原因になります。

安全な例です。

C#
Image? oldImage = pictureBox1.Image;

pictureBox1.Image = new Bitmap("new.jpg");

oldImage?.Dispose();

ファイル選択で画像を切り替える処理なら、次のように書けます。

C#
private void LoadImage(string path)
{
Image? oldImage = pictureBox1.Image;

pictureBox1.Image = new Bitmap(path);
pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;

oldImage?.Dispose();
}

フォームを閉じるときにも、表示中の画像を破棄します。

C#
protected override void OnFormClosed(FormClosedEventArgs e)
{
pictureBox1.Image?.Dispose();
pictureBox1.Image = null;

base.OnFormClosed(e);
}

9-5. 表示が更新されない場合の確認ポイント

Bitmapを書き換えたのに画面表示が更新されない場合は、次の点を確認します。

確認項目対策
PictureBoxに再設定していないpictureBox.Image = bitmap;を再設定する
再描画されていないpictureBox.Invalidate();を呼ぶ
古いBitmapを表示している新しいBitmapインスタンスを設定する
UIスレッド以外から更新しているInvokeを使ってUIスレッドで更新する
Dispose済みのBitmapを使っているDisposeのタイミングを見直す

PictureBoxに表示中のBitmapへ直接描画した場合は、Invalidateを呼びます。

C#
using (Graphics g = Graphics.FromImage((Bitmap)pictureBox1.Image))
{
g.DrawEllipse(Pens.Red, 20, 20, 100, 100);
}

pictureBox1.Invalidate();

UIスレッド以外から更新する場合は、次のようにします。

C#
this.Invoke(() =>
{
pictureBox1.Image = new Bitmap("sample.jpg");
});

10. Bitmapでよくあるエラーと解決策

10-1. A generic error occurred in GDI+の原因

A generic error occurred in GDI+は、C# Bitmapで非常によく見かけるエラーです。原因がメッセージから分かりにくいため、保存処理、ファイルロック、権限、画像形式を順番に確認します。

よくある原因は次のとおりです。

  • 保存先フォルダが存在しない

  • 保存先に書き込み権限がない

  • 読み込んだ元ファイルに上書き保存している

  • BitmapやStreamを先にDisposeしている

  • JPEG保存できないPixelFormatのまま保存している

  • ファイル名やパスが不正

  • 画像ファイルがロックされている

対策例です。

C#
string input = "input.jpg";
string output = @"C:\Output\output.jpg";

Directory.CreateDirectory(Path.GetDirectoryName(output)!);

using Bitmap source = new Bitmap(input);
using Bitmap converted = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);

using (Graphics g = Graphics.FromImage(converted))
{
g.DrawImage(source, 0, 0, source.Width, source.Height);
}

converted.Save(output, ImageFormat.Jpeg);

JPEGで保存するときは、透過ありの32bit画像やインデックスカラー画像をそのまま保存するより、24bit RGBへ変換してから保存すると安定しやすいです。

10-2. Parameter is not validが発生する原因

Parameter is not validは、Bitmap作成時やClone時、DrawImage時に発生することがあります。

代表的な原因です。

原因対策
ファイルが画像ではない拡張子だけでなく実際に読み込めるか確認する
ファイルが破損している例外処理を入れる
Streamが閉じられているBitmap使用中はStreamを閉じない、またはBitmapをコピーする
トリミング範囲が範囲外Rectangleを画像内に収める
幅や高さが0サイズ計算を見直す

トリミング前に範囲チェックする例です。

C#
static bool IsValidCropArea(Rectangle rect, Bitmap bitmap)
{
return rect.X >= 0
&& rect.Y >= 0
&& rect.Width > 0
&& rect.Height > 0
&& rect.Right <= bitmap.Width
&& rect.Bottom <= bitmap.Height;
}

使い方です。

C#
using Bitmap bitmap = new Bitmap("input.jpg");

Rectangle cropArea = new Rectangle(100, 100, 300, 300);

if (!IsValidCropArea(cropArea, bitmap))
{
Console.WriteLine("切り抜き範囲が不正です。");
return;
}

using Bitmap cropped = bitmap.Clone(cropArea, bitmap.PixelFormat);

10-3. ファイルが使用中で削除・上書きできない原因

画像ファイルが削除できない、上書きできない場合、多くはBitmapがファイルをロックしています。

問題になりやすいコードです。

C#
Bitmap bitmap = new Bitmap("sample.jpg");

// Disposeしていないため、ファイルがロックされ続ける可能性がある
File.Delete("sample.jpg");

正しい例です。

C#
using (Bitmap bitmap = new Bitmap("sample.jpg"))
{
Console.WriteLine(bitmap.Width);
}

// usingを抜けた後なら削除しやすい
File.Delete("sample.jpg");

元ファイルをすぐ削除したい場合は、ファイルロックを避ける読み込み方を使います。

C#
Bitmap LoadWithoutLock(string path)
{
byte[] bytes = File.ReadAllBytes(path);

using MemoryStream ms = new MemoryStream(bytes);
using Bitmap temp = new Bitmap(ms);

return new Bitmap(temp);
}

使い方です。

C#
using Bitmap bitmap = LoadWithoutLock("sample.jpg");

File.Delete("sample.jpg");

10-4. メモリ不足が発生するケースと対策

画像処理では、ファイルサイズよりも展開後のピクセルデータが重要です。たとえば、10000×10000ピクセルの32bit画像は、単純計算で約400MBのメモリを使います。

10000 × 10000 × 4バイト = 400,000,000バイト

複数枚を同時に読み込むと、すぐにメモリ不足になります。

対策は次のとおりです。

  • 必要な画像だけ読み込み、すぐDisposeする

  • 大量画像をリストに保持しない

  • 元画像、加工画像、出力画像を同時に持ちすぎない

  • 大きすぎる画像は処理前に制限する

  • 画像サイズの上限を決める

  • サーバーサイドではImageSharpやSkiaSharpも検討する

サイズ上限をチェックする例です。

C#
using Bitmap bitmap = new Bitmap("input.jpg");

long pixels = (long)bitmap.Width * bitmap.Height;

if (pixels > 50_000_000)
{
throw new InvalidOperationException("画像サイズが大きすぎます。");
}

大量画像を処理するときは、1枚ずつ読み込んで保存し、すぐ破棄します。

C#
foreach (string file in Directory.GetFiles("images", "*.jpg"))
{
using Bitmap bitmap = new Bitmap(file);

// 処理

string output = Path.Combine("output", Path.GetFileName(file));
bitmap.Save(output, ImageFormat.Jpeg);
}

10-5. System.Drawing.Common is not supported on this platformの対処法

.NET 6以降のMac、Linux、LinuxコンテナなどでC# Bitmapを使うと、次のようなエラーが発生することがあります。

System.Drawing.Common is not supported on non-Windows platforms.

これは、System.Drawing.CommonがWindows固有のライブラリとして扱われるようになったためです。Microsoftのドキュメントでも、.NET 6以降、非Windows環境では警告や実行時例外が発生することが説明されています。

対処法は次のいずれかです。

対処法内容
Windowsで実行するWindowsデスクトップアプリとして使う
net8.0-windowsなどにするWindows専用アプリであることを明示する
ImageSharpへ移行するクロスプラットフォームの画像処理に向く
SkiaSharpへ移行する高速描画やクロスプラットフォーム用途に向く
サーバー処理からSystem.Drawingを外すWeb APIやLinuxコンテナでは特に推奨

WindowsアプリならC# Bitmap、クロスプラットフォームなら別ライブラリ、という切り分けが重要です。

11. Bitmapを安全・効率的に扱うベストプラクティス

11-1. using文でリソースを確実に解放する

C# Bitmapでは、usingを徹底することが最も重要です。

C#
using Bitmap bitmap = new Bitmap("input.jpg");
using Graphics g = Graphics.FromImage(bitmap);
using Font font = new Font("Meiryo", 24);
using Brush brush = new SolidBrush(Color.White);

g.DrawString("Sample", font, brush, 10, 10);

bitmap.Save("output.jpg", ImageFormat.Jpeg);

Bitmapだけでなく、GraphicsPenBrushFontImageAttributesEncoderParametersなども破棄対象です。

避けたい書き方です。

C#
Bitmap bitmap = new Bitmap("input.jpg");
Graphics g = Graphics.FromImage(bitmap);

// Disposeされない

安全な書き方です。

C#
using Bitmap bitmap = new Bitmap("input.jpg");
using Graphics g = Graphics.FromImage(bitmap);

大量画像処理では、Disposeの有無が安定性に直結します。

11-2. 元画像と出力画像を同じパスにしない

元画像と同じパスに保存しようとすると、ファイルロックやGDI+エラーの原因になります。

避けたい例です。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");
bitmap.Save("sample.jpg", ImageFormat.Jpeg);

安全な例です。

C#
string input = "sample.jpg";
string temp = "sample_temp.jpg";

using (Bitmap bitmap = new Bitmap(input))
{
bitmap.Save(temp, ImageFormat.Jpeg);
}

File.Delete(input);
File.Move(temp, input);

さらに安全にするなら、バックアップを作ってから置き換えます。

C#
string backup = "sample_backup.jpg";

File.Copy(input, backup, overwrite: true);
File.Delete(input);
File.Move(temp, input);

上書き保存ではなく、「別ファイルに保存してから置き換える」設計にすると、トラブルを減らせます。

11-3. 画像処理の目的に合わせてPixelFormatを選ぶ

Bitmap作成時には、PixelFormatを指定できます。

C#
using Bitmap bitmap = new Bitmap(800, 600, PixelFormat.Format24bppRgb);

代表的なPixelFormatです。

PixelFormat用途
Format24bppRgbJPEG保存、透過なし画像
Format32bppArgbPNG保存、透過あり画像
Format32bppRgb32bitだがアルファなし
Format8bppIndexedパレット形式。扱いに注意
Format1bppIndexed白黒画像など

JPEG保存ならFormat24bppRgb、透過PNGならFormat32bppArgbが扱いやすいです。

透過あり画像を作る例です。

C#
using Bitmap bitmap = new Bitmap(500, 500, PixelFormat.Format32bppArgb);

using (Graphics g = Graphics.FromImage(bitmap))
{
g.Clear(Color.Transparent);
}

bitmap.Save("transparent.png", ImageFormat.Png);

JPEG用に変換する例です。

C#
using Bitmap source = new Bitmap("input.png");
using Bitmap jpegBitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format24bppRgb);

using (Graphics g = Graphics.FromImage(jpegBitmap))
{
g.Clear(Color.White);
g.DrawImage(source, 0, 0, source.Width, source.Height);
}

jpegBitmap.Save("output.jpg", ImageFormat.Jpeg);

透過PNGをJPEGに変換する場合、透明部分はそのまま保存できないため、背景色を白などで塗ってから保存します。

11-4. 大きな画像を扱うときのメモリ対策

大きな画像を扱う場合は、メモリ使用量に注意します。

実用上の対策は次のとおりです。

  • 画像を一括で読み込まず、1枚ずつ処理する

  • 加工後のBitmapをすぐ保存して破棄する

  • 必要以上に大きな中間Bitmapを作らない

  • 処理前にピクセル数の上限をチェックする

  • サムネイルだけ必要なら元画像を長時間保持しない

  • 例外発生時でもDisposeされるようにusingを使う

フォルダ内画像を1枚ずつ処理する例です。

C#
Directory.CreateDirectory("output");

foreach (string file in Directory.GetFiles("input", "*.jpg"))
{
using Bitmap source = new Bitmap(file);
using Bitmap resized = new Bitmap(300, 200);

using (Graphics g = Graphics.FromImage(resized))
{
g.DrawImage(source, 0, 0, 300, 200);
}

string outputPath = Path.Combine("output", Path.GetFileName(file));
resized.Save(outputPath, ImageFormat.Jpeg);
}

このように、ループの各回でBitmapを破棄することで、メモリ増加を抑えられます。

11-5. サーバーサイド画像処理での注意点

WebアプリやAPIで画像処理を行う場合、C# Bitmapは慎重に使う必要があります。特にLinuxコンテナやクラウド環境では、System.Drawing.Commonの非Windows制限に引っかかる可能性があります。

サーバーサイドで注意すべき点です。

  • Linux環境ではSystem.Drawing.Commonを前提にしない

  • アップロード画像のサイズ上限を設ける

  • 拡張子だけでなく画像として読み込めるか検証する

  • 処理後は必ずDisposeする

  • 保存先パスの権限を確認する

  • 同名ファイルの上書き競合を避ける

  • EXIF情報や回転情報の扱いに注意する

  • 大量同時処理ではメモリ使用量を監視する

Webアプリでクロスプラットフォームに対応したい場合は、ImageSharpやSkiaSharpを検討するほうが安全です。Windows専用の社内ツール、デスクトップアプリ、Windowsバッチ処理であれば、C# Bitmapは今でも扱いやすい選択肢です。

12. C# Bitmapの実用サンプル集

12-1. 画像を読み込んでPNG形式で保存するサンプル

JPEG画像を読み込んでPNG形式で保存するシンプルな例です。

C#
using System.Drawing;
using System.Drawing.Imaging;

string inputPath = "input.jpg";
string outputPath = "output.png";

using Bitmap bitmap = new Bitmap(inputPath);

bitmap.Save(outputPath, ImageFormat.Png);

ファイルロックを避けたい場合は、次のように読み込みます。

C#
using System.Drawing;
using System.Drawing.Imaging;

static Bitmap LoadWithoutLock(string path)
{
byte[] bytes = File.ReadAllBytes(path);

using MemoryStream ms = new MemoryStream(bytes);
using Bitmap temp = new Bitmap(ms);

return new Bitmap(temp);
}

using Bitmap bitmap = LoadWithoutLock("input.jpg");

bitmap.Save("output.png", ImageFormat.Png);

12-2. 画像に文字を入れて保存するサンプル

画像の左上に文字を入れて保存するサンプルです。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");

using (Graphics g = Graphics.FromImage(bitmap))
using (Font font = new Font("Meiryo", 36, FontStyle.Bold))
using (Brush textBrush = new SolidBrush(Color.White))
using (Brush shadowBrush = new SolidBrush(Color.FromArgb(160, Color.Black)))
{
string text = "Sample";

PointF shadowPoint = new PointF(32, 32);
PointF textPoint = new PointF(30, 30);

g.DrawString(text, font, shadowBrush, shadowPoint);
g.DrawString(text, font, textBrush, textPoint);
}

bitmap.Save("text-output.jpg", ImageFormat.Jpeg);

影を少しずらして描画することで、写真の上でも文字が見やすくなります。

12-3. 複数画像を1枚に合成するサンプル

2枚の画像を横に並べて1枚に合成する例です。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap left = new Bitmap("left.jpg");
using Bitmap right = new Bitmap("right.jpg");

int width = left.Width + right.Width;
int height = Math.Max(left.Height, right.Height);

using Bitmap canvas = new Bitmap(width, height);

using (Graphics g = Graphics.FromImage(canvas))
{
g.Clear(Color.White);

g.DrawImage(left, 0, 0, left.Width, left.Height);
g.DrawImage(right, left.Width, 0, right.Width, right.Height);
}

canvas.Save("merged.jpg", ImageFormat.Jpeg);

複数の画像をグリッド状に並べる場合は、行数・列数を計算して配置します。

C#
using System.Drawing;
using System.Drawing.Imaging;

string[] files = Directory.GetFiles("images", "*.jpg").Take(4).ToArray();

int thumbWidth = 300;
int thumbHeight = 200;
int columns = 2;
int rows = 2;

using Bitmap canvas = new Bitmap(thumbWidth * columns, thumbHeight * rows);

using (Graphics g = Graphics.FromImage(canvas))
{
g.Clear(Color.White);

for (int i = 0; i < files.Length; i++)
{
using Bitmap source = new Bitmap(files[i]);

int x = (i % columns) * thumbWidth;
int y = (i / columns) * thumbHeight;

g.DrawImage(source, x, y, thumbWidth, thumbHeight);
}
}

canvas.Save("grid.jpg", ImageFormat.Jpeg);

12-4. フォルダ内の画像を一括リサイズするサンプル

フォルダ内のJPEG画像を一括でリサイズするサンプルです。

C#
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

static Bitmap ResizeKeepAspect(Bitmap source, int maxWidth, int maxHeight)
{
double ratioX = (double)maxWidth / source.Width;
double ratioY = (double)maxHeight / source.Height;
double ratio = Math.Min(ratioX, ratioY);

int width = (int)(source.Width * ratio);
int height = (int)(source.Height * ratio);

Bitmap resized = new Bitmap(width, height, PixelFormat.Format24bppRgb);

using Graphics g = Graphics.FromImage(resized);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

g.DrawImage(source, 0, 0, width, height);

return resized;
}

string inputDir = "input";
string outputDir = "output";

Directory.CreateDirectory(outputDir);

foreach (string file in Directory.GetFiles(inputDir, "*.jpg"))
{
using Bitmap source = new Bitmap(file);
using Bitmap resized = ResizeKeepAspect(source, 800, 600);

string outputPath = Path.Combine(outputDir, Path.GetFileName(file));

resized.Save(outputPath, ImageFormat.Jpeg);
}

大量画像を処理する場合は、1枚ごとにusingで破棄することが重要です。

12-5. 画像をトリミングしてサムネイル化するサンプル

中央を正方形に切り抜いて、300×300のサムネイルを作るサンプルです。

C#
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

static Bitmap CreateSquareThumbnail(Bitmap source, int size)
{
int side = Math.Min(source.Width, source.Height);

int srcX = (source.Width - side) / 2;
int srcY = (source.Height - side) / 2;

Rectangle srcRect = new Rectangle(srcX, srcY, side, side);
Rectangle destRect = new Rectangle(0, 0, size, size);

Bitmap thumbnail = new Bitmap(size, size, PixelFormat.Format24bppRgb);

using Graphics g = Graphics.FromImage(thumbnail);

g.Clear(Color.White);
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
g.SmoothingMode = SmoothingMode.HighQuality;
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
g.CompositingQuality = CompositingQuality.HighQuality;

g.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel);

return thumbnail;
}

using Bitmap source = new Bitmap("input.jpg");
using Bitmap thumbnail = CreateSquareThumbnail(source, 300);

thumbnail.Save("thumbnail.jpg", ImageFormat.Jpeg);

プロフィール画像、商品一覧画像、ギャラリーのサムネイルなどに使いやすい処理です。

13. C# Bitmapに関するよくある質問

13-1. BitmapとImageはどちらを使うべき?

画像を読み込んで幅や高さを確認するだけなら、Imageでも問題ありません。一方、画像を作成する、ピクセルを操作する、描画する、リサイズするなどの処理ではBitmapを使うことが多いです。

C#
using Image image = Image.FromFile("sample.jpg");
Console.WriteLine(image.Width);

Bitmapを使う例です。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");

Color color = bitmap.GetPixel(10, 10);
bitmap.SetPixel(10, 10, Color.Red);

目安としては、画像全般として扱うならImage、具体的に加工するならBitmapです。

13-2. Bitmapを使うとファイルがロックされるのはなぜ?

new Bitmap("sample.jpg")のようにファイルパスから直接読み込むと、Bitmapが内部で元ファイルを参照し続けることがあります。そのため、BitmapをDisposeするまで、元ファイルを削除・上書きできない場合があります。

対策は次のとおりです。

C#
using Bitmap bitmap = new Bitmap("sample.jpg");

// 処理

または、ファイルロックを避けて読み込みます。

C#
byte[] bytes = File.ReadAllBytes("sample.jpg");

using MemoryStream ms = new MemoryStream(bytes);
using Bitmap temp = new Bitmap(ms);
using Bitmap bitmap = new Bitmap(temp);

元ファイルを加工後に置き換える場合は、別名保存してから差し替える設計にしましょう。

13-3. WebアプリでBitmapを使っても問題ない?

Windows環境で限定的に使うなら動作する場合もありますが、現在はサーバーサイド画像処理でC# Bitmapに依存する設計は慎重に考えるべきです。特にLinuxコンテナ、クラウド環境、クロスプラットフォーム運用では、System.Drawing.Commonの非Windows制限が問題になります。Microsoftもクロスプラットフォーム用途ではSkiaSharpやImageSharpなどへの移行を推奨しています。

Webアプリで画像アップロード、サムネイル作成、画像変換を行うなら、ImageSharpやSkiaSharpを検討するのが現実的です。

13-4. MacやLinuxでBitmapを使えない場合はどうする?

MacやLinuxでSystem.Drawing.Common is not supported on this platformが出る場合は、基本的に別ライブラリへ移行します。

候補は次のとおりです。

目的候補
画像のリサイズ・変換ImageSharp
高速描画・クロスプラットフォーム描画SkiaSharp
System.Drawingに近いAPIを使いたいAspose.Drawing
MAUI系の描画Microsoft.Maui.Graphics

.NET 6には一時的な互換スイッチがありましたが、Microsoftのドキュメントでは、そのスイッチは.NET 6のみで使え、.NET 7では削除されたと説明されています。

そのため、新規開発では非Windows環境でC# Bitmapを使い続けるより、最初からクロスプラットフォーム対応ライブラリを選ぶほうが安全です。

13-5. ImageSharpやSkiaSharpとの違いは?

C# Bitmap、ImageSharp、SkiaSharpの違いを簡単に整理すると次のようになります。

項目C# BitmapImageSharpSkiaSharp
主な用途Windows向け画像処理クロスプラットフォーム画像処理高速描画・画像処理
Windows対応得意対応対応
Mac/Linux対応.NET 6以降は注意対応対応
APIの親しみやすさ.NET標準に近く学びやすいC#らしく扱いやすい描画APIに慣れが必要
サーバー用途注意が必要向いている向いている
透過・リサイズ対応対応対応

WindowsフォームやWPFのデスクトップアプリならC# Bitmapは扱いやすいです。一方、Webアプリ、Linuxサーバー、Docker、クロスプラットフォームアプリでは、ImageSharpやSkiaSharpを選ぶほうがトラブルを避けやすくなります。

まとめ

C# Bitmapは、画像の読み込み、保存、描画、リサイズ、トリミング、ピクセル操作まで幅広く対応できる便利なクラスです。WindowsデスクトップアプリやWindows上の画像変換ツールでは、今でも実用的な選択肢です。

基本的な使い方は、次のようにシンプルです。

C#
using System.Drawing;
using System.Drawing.Imaging;

using Bitmap bitmap = new Bitmap("input.jpg");

bitmap.Save("output.png", ImageFormat.Png);

ただし、実用では次のポイントを必ず押さえておきましょう。

  • BitmapGraphicsPenBrushFontusingで破棄する

  • 元画像と同じパスに直接上書き保存しない

  • ファイルロックを避けたい場合は、Streamやバイト配列からコピーして読み込む

  • リサイズ時はInterpolationMode.HighQualityBicubicを使う

  • 大量ピクセル処理ではGetPixel/SetPixelではなくLockBitsを検討する

  • .NET 6以降の非Windows環境ではSystem.Drawing.Commonの制限に注意する

  • Webアプリやクロスプラットフォーム用途ではImageSharpやSkiaSharpも検討する

C# Bitmapは、正しくDisposeし、保存パスや実行環境に注意すれば、画像処理を手軽に実装できる強力な機能です。まずは画像の読み込み・保存・描画・リサイズから試し、必要に応じてトリミングやピクセル操作へ広げていくと理解しやすいでしょう。