Dan Byström’s Bwain

Blog without an interesting name

Archive for August, 2008

Soft edged images in GDI+

Posted by Dan Byström on August 24, 2008

Last week I felt a sudden urge to create bitmaps with rounded corners and soft edges. Partly because I thought it would look nice, but mostly just as an intellectual exercise. (And also, according to Joel Spolsky, much of the success behind the iPod and iPhone can be attributed to their rounded corner design. That’s the best explanation I have come across, anyway!!! So, rounded corners sell! 🙂 )

Here are some sample pictures:

  1. Ordinary Graphics.DrawImage
  2. We can use a ColorMatrix to draw a semi-transparent image. However, I can’t figure out a way to use this technique to achieve the result I’m after.
  3. This is what I’m trying to achieve. What shall I call it? Smooth edges? Soft edges? Fluffy edges?
  4. In order to achieve 3) I’m about to inject this mask into the original image’s alpha channel.
  5. Although not discussed anymore in the article, we can use Graphics.SetClip to draw an image with round corners, but note the jaggedness at the corners. There may be some way to create anti-aliased clipping regions. If so, please drop a comment and tell me!
  6. Here I have used the technique which I’m discussing here (which produced 3) but without the “fluff” (a PathGradientBrush to be exact). Notice the smooth corners.

After spending way to much time figuring out a “pure” way to do this in GDI+, I gave up and decided to use LockBits to directly manipulate the pixels in the image. This is actually very straightforward and easy, but I can’t help feeling that there ought to be another way. If you know one, please drop a comment here. Using LockBits will result in an IntPtr pointing to the actual bits, leaving us with some different ways to access them:

  1. System.Runtime.InteropServices.Marshal.Copy. We can copy the pixels into a byte array, manipulate them and then copy them back. Unless we´re batch processing large amount of 16 mega pixels images I’ll guess we won’t notice much performance degradation, but it feels a little backward. If we’re going for a pure VB.NET solution, this is our only option I guess.
  2. The unsafe keyword in C#. This is just perfect, if it wasn’t for my previous traumatic experience with how .NET restricts unsafe assemblies. Anyway, this is what I’ll use in the sample code below.
  3. C++ with Managed Extensions. I suspect that most .NET programmers feel uneasy with this, but I think this is a much underestimated option. I’m not kidding you when I say that it actually takes less than ten minutes to add a new C++ project to your Visual Studio solution and write a C++ version of the unsafe code that I’m about to use. A few years ago I wrote a short tutorial on this.

The strategy I decided to go for is simple:

  1. Create a mask like the one in picture 4). This is done by the methods createRoundRect and createFluffyBrush below. The meaning of this mask is simple: the darker a pixel is, the more transparent shall the real image become in that particular place. And vice versa: the lighter a pixel is, the more opaque shall the image become in that part. Is is no coincidence that this is just the way the alpha channel works. 🙂
  2. Then I create a new bitmap as an exact copy of the original image (if I have more use of the original image that is, otherwise this can be skipped).
  3. Finally, I take one of the red, blue or green channels (irrelevant which one) from the mask and copy it into the new bitmap’s alpha channel! This is done by transferOneARGBChannelFromOneBitmapToAnother (surprise!).
  4. Done

So, here are the three helper methods:


        static public GraphicsPath createRoundRect( int x, int y, int width, int height, int radius )
        {
            GraphicsPath gp = new GraphicsPath();

            if (radius == 0)
                gp.AddRectangle( new Rectangle( x, y, width, height ) );
            else
            {
                gp.AddLine( x + radius, y, x + width - radius, y );
                gp.AddArc( x + width - radius, y, radius, radius, 270, 90 );
                gp.AddLine( x + width, y + radius, x + width, y + height - radius );
                gp.AddArc( x + width - radius, y + height - radius, radius, radius, 0, 90 );
                gp.AddLine( x + width - radius, y + height, x + radius, y + height );
                gp.AddArc( x, y + height - radius, radius, radius, 90, 90 );
                gp.AddLine( x, y + height - radius, x, y + radius );
                gp.AddArc( x, y, radius, radius, 180, 90 );
                gp.CloseFigure();
            }
            return gp;
        }


		public static Brush createFluffyBrush(
			GraphicsPath gp,
			float[] blendPositions,
			float[] blendFactors )
		{
			PathGradientBrush pgb = new PathGradientBrush( gp );
			Blend blend = new Blend();
			blend.Positions = blendPositions;
			blend.Factors = blendFactors;
			pgb.Blend = blend;
			pgb.CenterColor = Color.White;
			pgb.SurroundColors = new Color[] { Color.Black };
			return pgb;
		}

		public enum ChannelARGB
		{
			Blue = 0,
			Green = 1,
			Red = 2,
			Alpha = 3
		}

		public static void transferOneARGBChannelFromOneBitmapToAnother(
			Bitmap source,
			Bitmap dest,
			ChannelARGB sourceChannel,
			ChannelARGB destChannel )
		{
			if ( source.Size!=dest.Size )
				throw new ArgumentException();
			Rectangle r = new Rectangle( Point.Empty, source.Size );
			BitmapData bdSrc = source.LockBits( r, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb );
			BitmapData bdDst = dest.LockBits( r, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb );
			unsafe
			{
				byte* bpSrc = (byte*)bdSrc.Scan0.ToPointer();
				byte* bpDst = (byte*)bdDst.Scan0.ToPointer();
				bpSrc += (int)sourceChannel;
				bpDst += (int)destChannel;
				for ( int i = r.Height * r.Width; i > 0; i-- )
				{
					*bpDst = *bpSrc;
					bpSrc += 4;
					bpDst += 4;
				}
			}
			source.UnlockBits( bdSrc );
			dest.UnlockBits( bdDst );
		}

I’m not about to explain how any of the above methods work. If you don’t understand… just google for any of the keywords and you’ll find tons of tutorials. If something is still unclear, drop a comment here and I’ll see what I can do. The purpose of this article is the simple idea that you can inject a mask created with normal GDI+ operations into an image’s alpha channel, making that image transparent, semi-transparent or opaque exactly where you want it to. So now we just put it all together:

			Bitmap bmpFluffy = new Bitmap( bmpOriginal );
			Rectangle r = new Rectangle( Point.Empty, bmpFluffy.Size );

			using ( Bitmap bmpMask = new Bitmap( r.Width, r.Height ) )
			using ( Graphics g = Graphics.FromImage( bmpMask ) )
			using ( GraphicsPath path = createRoundRect(
				r.X, r.Y,
				r.Width, r.Height,
				Math.Min( r.Width, r.Height ) / 5 ) )
			using ( Brush brush = createFluffyBrush(
				path,
				new float[] { 0.0f, 0.1f, 1.0f },
				new float[] { 0.0f, 0.95f, 1.0f } ) )
			{
				g.FillRectangle( Brushes.Black, r );
				g.SmoothingMode = SmoothingMode.HighQuality;
				g.FillPath( brush, path );
				transferOneARGBChannelFromOneBitmapToAnother(
					bmpMask,
					bmpFluffy,
					ChannelARGB.Blue,
					ChannelARGB.Alpha );
			}
			// bmpFluffy is now powered up and ready to be used

The code above is sprinkled with magic numbers, so you can tell that it’s not production code! 🙂

  • Math.Min( r.Width, r.Height ) / 5. This is just may way of saying that I want the size of the rounded corners to be 20% the size of the shortest bitmap side.
  • new float[] { 0.0f, 0.1f, 1.0f } and float[] { 0.0f, 0.95f, 1.0f } this controls how much “fluff” I want at the edges. For example, you may find that new float[] { 0.0f, 0.1f, 0.2, 1.0f } and float[] { 0.0f, 0.9f, 1.0f, 1.0f } suits you better!
  • The arguments to transferOneARGBChannelFromOneBitmapToAnother. Why do I copy the blue channel??? Well, red or green will just just fine too! Since we have painted only gray scale values to bmpMask, the red, green and blue channels will be identical!

And of course, the mask doesn’t have to be created this way. Among other things, you could save a predefined Edge from Paint Shop Pro’s Picture Frames and load it into your app.

Happy coding! Ekeforshus

Posted in .NET, GDI+, Programming | 9 Comments »