using System;
using System.Drawing;
using System.Drawing.Imaging;
using NUnit.Framework;
namespace TIASshot.Tests {
/// <summary>
/// BitmapUtil.FastClone のテスト(ピュアロジック層)。
///
/// GUIDE_08: FastClone は Bitmap の画素バッファを LockBits + memcpy で複製する
/// ハードウェア非依存の処理であり,NUnit で仕様どおりの入出力を検証できる。
///
/// 対象外について:
/// 本コミット(609baf6・252cde3)の本質である Form1.ShowImage / PreviewMonitor の
/// タイマー駆動描画・_dispBufLock を介したクロススレッド Dispose の所有権モデル・
/// Lucam コールバックスレッドと UI スレッドの GDI 競合・WM_PAINT/WM_TIMER の
/// スケジューリングは,実メッセージループ・実カメラ・副ディスプレイに依存する
/// ハードウェア結合層(GUIDE_08)であり,NUnit の自動ユニットテストの対象外とする。
/// これらは実機の MEMMONITOR 計測(TEST_03 参照)で反復検証済み。
/// フェイクの UI ループを組んでまで検証する脆いテストは作らない。
/// </summary>
[TestFixture]
public class BitmapUtilTests {
// ---------------------------------------------------------------
// 仕様1・2: サイズ・PixelFormat の一致/全画素一致
// ---------------------------------------------------------------
/// <summary>
/// FastClone_24bppRgb_サイズとPixelFormatが入力と一致する
/// </summary>
[Test]
public void FastClone_24bppRgb_サイズとPixelFormatが入力と一致する() {
using (var src = CreateSolidBitmap(4, 3, PixelFormat.Format24bppRgb, Color.FromArgb(10, 20, 30)))
using (var dst = BitmapUtil.FastClone(src)) {
Assert.AreEqual(src.Width, dst.Width, "幅が一致すること");
Assert.AreEqual(src.Height, dst.Height, "高さが一致すること");
Assert.AreEqual(src.PixelFormat, dst.PixelFormat, "PixelFormat が一致すること");
}
}
/// <summary>
/// FastClone_24bppRgb_全画素が入力と一致する
/// </summary>
[Test]
public void FastClone_24bppRgb_全画素が入力と一致する() {
using (var src = CreateGradientBitmap(8, 5, PixelFormat.Format24bppRgb))
using (var dst = BitmapUtil.FastClone(src)) {
AssertPixelsEqual(src, dst);
}
}
/// <summary>
/// FastClone_32bppArgb_サイズとPixelFormatが入力と一致する
/// </summary>
[Test]
public void FastClone_32bppArgb_サイズとPixelFormatが入力と一致する() {
using (var src = CreateSolidBitmap(5, 4, PixelFormat.Format32bppArgb, Color.FromArgb(200, 10, 20, 30)))
using (var dst = BitmapUtil.FastClone(src)) {
Assert.AreEqual(src.Width, dst.Width, "幅が一致すること");
Assert.AreEqual(src.Height, dst.Height, "高さが一致すること");
Assert.AreEqual(src.PixelFormat, dst.PixelFormat, "PixelFormat が一致すること");
}
}
/// <summary>
/// FastClone_32bppArgb_全画素(アルファ含む)が入力と一致する
/// </summary>
[Test]
public void FastClone_32bppArgb_全画素が入力と一致する() {
using (var src = CreateGradientBitmap(6, 6, PixelFormat.Format32bppArgb))
using (var dst = BitmapUtil.FastClone(src)) {
AssertPixelsEqual(src, dst);
}
}
// ---------------------------------------------------------------
// 仕様3: 入力とは独立した画素バッファを持つ
// ---------------------------------------------------------------
/// <summary>
/// FastClone_複製後に入力をDisposeしても複製は有効なまま読める
/// </summary>
[Test]
public void FastClone_複製後に入力をDisposeしても複製は有効なまま読める() {
var src = CreateSolidBitmap(4, 4, PixelFormat.Format24bppRgb, Color.FromArgb(11, 22, 33));
Bitmap dst = BitmapUtil.FastClone(src);
src.Dispose();
try {
Assert.AreEqual(4, dst.Width, "src Dispose 後も dst の幅が読めること");
Assert.AreEqual(4, dst.Height, "src Dispose 後も dst の高さが読めること");
Color px = dst.GetPixel(0, 0);
Assert.AreEqual(11, px.R, "src Dispose 後も dst の画素(R)が読めること");
Assert.AreEqual(22, px.G, "src Dispose 後も dst の画素(G)が読めること");
Assert.AreEqual(33, px.B, "src Dispose 後も dst の画素(B)が読めること");
} finally {
dst.Dispose();
}
}
/// <summary>
/// FastClone_複製の画素を書き換えても入力の画素は変化しない
/// </summary>
[Test]
public void FastClone_複製の画素を書き換えても入力の画素は変化しない() {
using (var src = CreateSolidBitmap(4, 4, PixelFormat.Format24bppRgb, Color.FromArgb(50, 60, 70)))
using (var dst = BitmapUtil.FastClone(src)) {
dst.SetPixel(0, 0, Color.FromArgb(0, 0, 0));
Color srcPx = src.GetPixel(0, 0);
Assert.AreEqual(50, srcPx.R, "dst 変更後も src の R は不変");
Assert.AreEqual(60, srcPx.G, "dst 変更後も src の G は不変");
Assert.AreEqual(70, srcPx.B, "dst 変更後も src の B は不変");
}
}
// ---------------------------------------------------------------
// 仕様4: 入力を破壊しない(呼び出し前後で src の画素が不変)
// ---------------------------------------------------------------
/// <summary>
/// FastClone_呼び出し前後でsrcの画素が変化しない
/// </summary>
[Test]
public void FastClone_呼び出し前後でsrcの画素が変化しない() {
using (var src = CreateGradientBitmap(7, 5, PixelFormat.Format24bppRgb)) {
// 呼び出し前の画素を保持
Color before00 = src.GetPixel(0, 0);
Color beforeCorner = src.GetPixel(src.Width - 1, src.Height - 1);
using (var dst = BitmapUtil.FastClone(src)) {
Assert.IsNotNull(dst);
}
Color after00 = src.GetPixel(0, 0);
Color afterCorner = src.GetPixel(src.Width - 1, src.Height - 1);
Assert.AreEqual(before00, after00, "FastClone 呼び出し前後で (0,0) の画素が不変であること");
Assert.AreEqual(beforeCorner, afterCorner, "FastClone 呼び出し前後で末尾画素が不変であること");
}
}
// ---------------------------------------------------------------
// 仕様5: ストライドのパディング(幅が4の倍数でない24bppRgb)
// ---------------------------------------------------------------
/// <summary>
/// FastClone_幅が4の倍数でない24bppRgb_全画素が一致する(行末パディングを除外して比較)
/// </summary>
[Test]
public void FastClone_幅が4の倍数でない24bppRgb_全画素が一致する() {
// 幅5px * 3byte = 15byte → 4byte境界にするため1byteパディングが入るサイズ
using (var src = CreateGradientBitmap(5, 3, PixelFormat.Format24bppRgb))
using (var dst = BitmapUtil.FastClone(src)) {
AssertPixelsEqual(src, dst);
}
}
/// <summary>
/// FastClone_幅が4の倍数でない24bppRgb_サイズが一致する
/// </summary>
[Test]
public void FastClone_幅が4の倍数でない24bppRgb_サイズが一致する() {
using (var src = CreateGradientBitmap(5, 3, PixelFormat.Format24bppRgb))
using (var dst = BitmapUtil.FastClone(src)) {
Assert.AreEqual(5, dst.Width);
Assert.AreEqual(3, dst.Height);
}
}
// ---------------------------------------------------------------
// 仕様6: インデックスカラーのフォールバック
// ---------------------------------------------------------------
/// <summary>
/// FastClone_8bppIndexed_色が等価な独立コピーを返す:
///
/// 8bppIndexed は LockBits 単純コピーではパレットを保持できないため,
/// `new Bitmap(src)`(GDI+ フルコピー)にフォールバックする。
/// このフォールバックは GDI+ の仕様上,返り値の PixelFormat を
/// Format32bppArgb に変換する(Format8bppIndexed のままにはならない)。
/// そのため本テストでは PixelFormat の一致は検証せず,
/// 「例外を投げない」「入力とは別インスタンス」「サイズが一致する」
/// 「解決済みの色(GetPixel が返すパレット適用後の ARGB)が画素ごとに一致する」
/// という,このフォールバックが満たすべき妥当な契約を検証する。
/// </summary>
[Test]
public void FastClone_8bppIndexed_色が等価な独立コピーを返す() {
using (var src = new Bitmap(4, 4, PixelFormat.Format8bppIndexed)) {
// パレットを設定(グレースケール)
ColorPalette palette = src.Palette;
for (int i = 0; i < palette.Entries.Length; i++) {
palette.Entries[i] = Color.FromArgb(i, i, i);
}
src.Palette = palette;
// 画素インデックスを設定
SetIndexedPixel(src, 0, 0, 5);
SetIndexedPixel(src, 1, 1, 200);
SetIndexedPixel(src, 3, 3, 255);
Bitmap dst = null;
Assert.DoesNotThrow(() => dst = BitmapUtil.FastClone(src),
"8bppIndexed を渡しても例外をスローしないこと");
try {
Assert.IsNotNull(dst, "8bppIndexed のフォールバックコピーは null であってはならない");
Assert.AreNotSame(src, dst, "入力とは別インスタンスであること");
Assert.AreEqual(src.Width, dst.Width, "幅が一致すること");
Assert.AreEqual(src.Height, dst.Height, "高さが一致すること");
// 解決済みの色(パレット適用後の ARGB)が画素ごとに一致すること
Assert.AreEqual(src.GetPixel(0, 0), dst.GetPixel(0, 0), "(0,0) の色が一致すること");
Assert.AreEqual(src.GetPixel(1, 1), dst.GetPixel(1, 1), "(1,1) の色が一致すること");
Assert.AreEqual(src.GetPixel(3, 3), dst.GetPixel(3, 3), "(3,3) の色が一致すること");
// 独立性: src を Dispose 後も dst の画素が読めること
} finally {
if (dst != null) dst.Dispose();
}
}
}
/// <summary>
/// FastClone_8bppIndexed_複製後に入力をDisposeしても複製の色は読める:
/// インデックスフォールバック経路でも独立性(クロススレッド Dispose 耐性)が
/// 保たれることを確認する。
/// </summary>
[Test]
public void FastClone_8bppIndexed_複製後に入力をDisposeしても複製の色は読める() {
var src = new Bitmap(2, 2, PixelFormat.Format8bppIndexed);
ColorPalette palette = src.Palette;
palette.Entries[9] = Color.FromArgb(9, 9, 9);
src.Palette = palette;
SetIndexedPixel(src, 0, 0, 9);
Bitmap dst = BitmapUtil.FastClone(src);
Color expected = src.GetPixel(0, 0);
src.Dispose();
try {
Color actual = dst.GetPixel(0, 0);
Assert.AreEqual(expected, actual, "src Dispose 後も dst の色が読めること");
} finally {
dst.Dispose();
}
}
// ---------------------------------------------------------------
// 仕様7: null 入力
// ---------------------------------------------------------------
/// <summary>
/// FastClone_null入力_nullを返す
/// </summary>
[Test]
public void FastClone_null入力_nullを返す() {
Bitmap result = BitmapUtil.FastClone(null);
Assert.IsNull(result, "null 入力に対して null を返すこと(例外をスローしない)");
}
// ---------------------------------------------------------------
// ヘルパー
// ---------------------------------------------------------------
/// <summary>
/// 単色の Bitmap を生成する
/// </summary>
private static Bitmap CreateSolidBitmap(int width, int height, PixelFormat format, Color color) {
var bmp = new Bitmap(width, height, format);
using (var g = Graphics.FromImage(bmp)) {
using (var brush = new SolidBrush(color)) {
g.FillRectangle(brush, 0, 0, width, height);
}
}
return bmp;
}
/// <summary>
/// 決定的なグラデーション(座標に依存する画素値)を持つ Bitmap を GetPixel/SetPixel で生成する。
/// GDI+ の描画経路を通さず,画素値を直接指定するため決定的。
/// </summary>
private static Bitmap CreateGradientBitmap(int width, int height, PixelFormat format) {
var bmp = new Bitmap(width, height, format);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = (x * 37 + y * 11) % 256;
int g = (x * 5 + y * 53) % 256;
int b = (x * 71 + y * 3) % 256;
int a = format == PixelFormat.Format32bppArgb ? (x * 13 + y * 17) % 256 : 255;
bmp.SetPixel(x, y, Color.FromArgb(a, r, g, b));
}
}
return bmp;
}
/// <summary>
/// 8bppIndexed Bitmap の指定座標にパレットインデックスを設定する(LockBits 経由)
/// </summary>
private static void SetIndexedPixel(Bitmap bmp, int x, int y, byte index) {
var rect = new Rectangle(x, y, 1, 1);
BitmapData data = bmp.LockBits(rect, ImageLockMode.WriteOnly, bmp.PixelFormat);
try {
System.Runtime.InteropServices.Marshal.WriteByte(data.Scan0, index);
} finally {
bmp.UnlockBits(data);
}
}
/// <summary>
/// 2つの Bitmap が全画素で一致することを検証する(GetPixel による代表的座標比較でなく全画素走査)。
/// LockBits で生バイトを取得し,行ごとに有効バイト幅(パディング除外)だけを比較する。
/// </summary>
private static void AssertPixelsEqual(Bitmap expected, Bitmap actual) {
Assert.AreEqual(expected.Width, actual.Width, "幅が一致すること(画素比較の前提)");
Assert.AreEqual(expected.Height, actual.Height, "高さが一致すること(画素比較の前提)");
Assert.AreEqual(expected.PixelFormat, actual.PixelFormat, "PixelFormat が一致すること(画素比較の前提)");
var rect = new Rectangle(0, 0, expected.Width, expected.Height);
BitmapData expData = expected.LockBits(rect, ImageLockMode.ReadOnly, expected.PixelFormat);
BitmapData actData = actual.LockBits(rect, ImageLockMode.ReadOnly, actual.PixelFormat);
try {
int bytesPerPixel = Image.GetPixelFormatSize(expected.PixelFormat) / 8;
int validBytesPerRow = expected.Width * bytesPerPixel;
var expRow = new byte[validBytesPerRow];
var actRow = new byte[validBytesPerRow];
for (int y = 0; y < expected.Height; y++) {
System.Runtime.InteropServices.Marshal.Copy(
IntPtr.Add(expData.Scan0, y * expData.Stride), expRow, 0, validBytesPerRow);
System.Runtime.InteropServices.Marshal.Copy(
IntPtr.Add(actData.Scan0, y * actData.Stride), actRow, 0, validBytesPerRow);
for (int i = 0; i < validBytesPerRow; i++) {
Assert.AreEqual(expRow[i], actRow[i],
$"行 y={y} のバイト位置 {i}(パディング除外後)が一致すること");
}
}
} finally {
expected.UnlockBits(expData);
actual.UnlockBits(actData);
}
}
}
}