Winformsで自分用のペイントソフトつくるよ！(3)

## 何の話か？

日記です．以下の話の続き．

* [～つくるよ](https://qiita.com/NyancoRitter/items/51caea3b59ebc4da9f2c)
* [～つくるよ(2)](https://qiita.com/NyancoRitter/items/66fe8371c2ffdfd72d00)

## フリーズかよ！というほど重かった．びっくりした．

自由形状の選択ができているとおもっていたら，実は選択領域が小さいときにしか通用しない実装になっていた．
でかい範囲を選択してドラッグした瞬間に **20秒くらい固まる** 有様になっていた．

例えばこの絵の黄色い部分みたいな感じの範囲選択とかすると，もう大変なことになっていた．
![Fig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3574818/4aaa73c1-f590-4f09-b981-eacdfb7c8182.png)

### 原因

選択範囲外を透明にした画像を作るために，選択範囲を包括する矩形領域内の画素を走査して `GraphicsPath.IsVisible()` と `GraphicsPath.IsOutlineVisible()` で各画素を透明にするか否かを決めていたのだが，こいつらの処理がほんとにほんとに圧倒的に重い．

だいたいこんな実装になっていた：
```CSharp
/// <summary>
/// 現在の画像の部分画像を生成して返す．
/// 画像サイズは引数を包括する矩形のサイズとなる．
/// フォーマットは Format32bppArgb となり，引数により指定された範囲の外側は透明となる．
/// </summary>
/// <param name="Path">範囲指定</param>
/// <returns></returns>
public Bitmap CreatePartialImg( System.Drawing.Drawing2D.GraphicsPath Path )
{
	//※ m_BMP というのが「現在の画像」．Bitmap型．

	var AABB = Rectangle.Round( Path.GetBounds() );
	++AABB.Width;
	++AABB.Height;
	AABB.Intersect( new Rectangle( new Point(0,0), m_BMP.Size ) );
	var BMP = m_BMP.Clone( AABB, System.Drawing.Imaging.PixelFormat.Format32bppArgb );

	const int BPP = 4;	//Bytes per Pixel
	System.Drawing.Imaging.BitmapData bmpData = BMP.LockBits( new Rectangle( 0,0, BMP.Width,BMP.Height ), System.Drawing.Imaging.ImageLockMode.ReadWrite, BMP.PixelFormat );
	
	unsafe
	{
		IntPtr ptr = bmpData.Scan0;
		for( int y=0; y<BMP.Height; ++y )
		{
			int SrcY = y + AABB.Top;
			Byte* p = (Byte*)ptr.ToPointer() + bmpData.Stride*y;
			for( int x=0; x<BMP.Width; ++x )
			{
				int SrcX = x + AABB.Left;
				if( !Path.IsVisible( SrcX, SrcY )  &&  !Path.IsOutlineVisible( SrcX,SrcY, Pens.Gray ) )
				{	p[3] = 0;	}

				p += BPP;
			}
		}
	}
	BMP.UnlockBits(bmpData);
	return BMP;
}
```

※引数の `Path` の外周も「領域内」としたいので，`AABB`のサイズに`++` してたり，`Path.IsOutlineVisible()` を使ったりとかしている．

### 解決した方法

最初は `GraphicsPath` から `Region` を作って，`Region.IsVisible()` を使うことを試みた．こっちなら大分軽いのだが，`Region` には `IsOutlineVisible()` 相当の物が無いため，処理結果が同じにできなかった．
↓
なので，画像バッファを用意して，そこに `FillPath` と `DrawPath` とで「不透明にする範囲」を塗りつぶした形の描画を行い，上記の走査時にはこの画像バッファの色を見て判断するようにした．

```CSharp
public Bitmap CreatePartialImg( System.Drawing.Drawing2D.GraphicsPath Path )
{
	var AABB = Rectangle.Round( Path.GetBounds() );
	++AABB.Width;
	++AABB.Height;
	AABB.Intersect( new Rectangle( new Point(0,0), m_BMP.Size ) );
	var BMP = m_BMP.Clone( AABB, System.Drawing.Imaging.PixelFormat.Format32bppArgb );

	const int BPP = 4;	//Bytes per Pixel
	System.Drawing.Imaging.BitmapData bmpData = BMP.LockBits( new Rectangle( 0,0, BMP.Width,BMP.Height ), System.Drawing.Imaging.ImageLockMode.ReadWrite, BMP.PixelFormat );

	//引数 Path の範囲内（境界含）を塗りつぶしたマスク画像をつくるよ！
	var Mask = new Bitmap(BMP.Width, BMP.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
	using( var g = Graphics.FromImage( Mask ) )
	{
		g.Clear( Color.Black );
		g.TranslateTransform( -AABB.Left, -AABB.Top );
		g.FillPath( Brushes.White, Path );
		g.DrawPath( Pens.White, Path );
	}
	System.Drawing.Imaging.BitmapData MaskData = Mask.LockBits( new Rectangle( 0,0, BMP.Width,BMP.Height ), System.Drawing.Imaging.ImageLockMode.ReadOnly, Mask.PixelFormat );

	unsafe
	{
		IntPtr ptr = bmpData.Scan0;
		IntPtr pMask = MaskData.Scan0;
		for( int y=0; y<BMP.Height; ++y )
		{
			Byte* p = (Byte*)ptr.ToPointer() + bmpData.Stride*y;
			Byte* pM = (Byte*)pMask.ToPointer() + MaskData.Stride*y;
			for( int x=0; x<BMP.Width; ++x )
			{
				if( pM[0]==0 )  //てきとーな判定
				{	p[3] = 0;	}

				p += BPP;
				pM += 3;
			}
		}
	}
	BMP.UnlockBits(bmpData);
	Mask.UnlockBits(MaskData);
	Mask.Dispose();
	return BMP;
}
```

「いや，そのマスク画像に 24bit とか絶対要らないでしょ常考…」とかはあるけどもそこは後々直すのだとして，とりあえずこれで実用的な処理速度には改善できた様子．よかった．
一時はどうなることかと思った．