Posts for May 2008

How to create an ASP .NET Captcha Control (part 2)

This is the second in a 3 part series on how to create an ASP .NET Captcha control. The previous post can be found here. This time we will look at how the CAPTCHA image can be generated using the built-in .NET framework classes.

Image generation approach
As described in the first post, the idea is to create an image showing a word, and let the user repeat it by typing it into a textbox. The image should be hard to read for OCR software, so that the CAPTCHA is hard to beat for automated bots. The way we will be doing this, is by stretching and warping the text, and adding noise. Luckily, this is easy to accomplish by using the GraphicsPath class to draw the string, and then use the Warp method on the GraphicsPath object.

Generating the image: Step by step
The first step in generating the image is to create a Bitmap object with the appropiate dimensions. We will also need a Font for the text, and a Brush for painting the text. I also declare a rectangle that is slightly smaller than the actual image, which will be used as the drawing bounds later. This will help to ensure, that the text fits on the image after the transformations:

        /// 
        /// Generates the CAPTCHA image.
        /// 
        /// 
        public static byte[] GenerateImage()
        {
            // Create image.
            var image = new Bitmap(size.Width, size.Height, PixelFormat.Format24bppRgb);
            var imgRectangle = new Rectangle(10, 10, image.Width - 10, image.Height - 10);
            // Get font and brush.
            var brush = new HatchBrush(HatchStyle.SolidDiamond, Color.Black, Color.FromArgb(rand.Next(160),rand.Next(160),rand.Next(160)));
            var font = new Font(fonts[rand.Next(fonts.Length - 1)], imgRectangle.Height, FontStyle.Italic, GraphicsUnit.Pixel);
 
Notice that we use a HatchBrush so that the word will be drawn using a hatch pattern. Ensuring that the text is not solid color, will help defeat OCR attacks. The actual font used is also chosen at random from a predefined list.
				
The next step is to get a Graphics object from the image, and use it to draw on the image. We'll wrap the code using the Graphics object in a using region to ensure that the instance is disposed as soon as we don't need it more. We then fill the background with white color and create a GraphicsPath instance, to which the selected Captcha word is added using the AddString method. The path object can now be warped, by stretching the corners a random amount. We also rotate the text a bit (between --10 and 10 degrees):

 
            // draw on the image.
            using(Graphics g = Graphics.FromImage(image))
            {
                g.FillRectangle(Brushes.WhiteSmoke, 0, 0, image.Width, image.Height);
                var path = new GraphicsPath();
                // Make sure text fits
                while (g.MeasureString(CaptchaWord, font).Width > imgRectangle.Width)
                    font = new Font(font.FontFamily, font.Size - 1, font.Style);
                
                path.AddString(CaptchaWord, font.FontFamily, (int)font.Style, font.Size, imgRectangle, StringFormat.GenericDefault);
                float v = 4;
                var warpPoints = new PointF[]
                                           {
                                                new PointF(rand.Next(imgRectangle.Width) / v,  rand.Next(imgRectangle.Height) / v),
                                                new PointF(imgRectangle.Width - rand.Next(imgRectangle.Width) / v,  rand.Next(imgRectangle.Height) / v),
                                                new PointF(rand.Next(imgRectangle.Width)/v, imgRectangle.Height - rand.Next(imgRectangle.Height) / v), 
                                                new PointF(imgRectangle.Width - rand.Next(imgRectangle.Width) / v, imgRectangle.Height - rand.Next(imgRectangle.Height)/ v) 
                                            };
                var warpMatrix = new Matrix();
                warpMatrix.Rotate(rand.Next(20) - 10);
                path.Warp(warpPoints, imgRectangle, warpMatrix, WarpMode.Perspective);                
                g.FillPath(brush, path);
 
The next step is to add a bit of noise to the image. This is done by drawing some small elipses (dots) randomly in the image, with a random color. This is implemented with a LINQ query selecting the details for each random dot:
 
                // Add some noise.
                var noise = from e in Enumerable.Range(0, NoiseAmount)
                         select new
                                    {
                                        X = rand.Next(image.Width),
                                        Y = rand.Next(image.Height),
                                        R = 1f + (float)rand.NextDouble() * 3f,
                                        Brush = new SolidBrush(Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255)))
                                    };
 
                foreach (var p in noise)
                    g.FillEllipse(p.Brush, p.X, p.Y, p.R, p.R);
            }
 
Finally, the resulting image is saved to PNG format in-memory and returned from the method. 
 
   // Save to buffer and return raw png image bytes.
            using(var buffer = new MemoryStream())
            {
                image.Save(buffer, ImageFormat.Png);
                return buffer.GetBuffer();
            }
 
We will use a custom http handler by implementing IHttpHandler to send the image to the client. This will be the subject for the next post in the series.