Well as no-one answered in the time it took to build my own I though I'd tell you how I did it instead. My application already contains an UI component for a number key pad. I've reused this and written my own code to generate a number Captcha image. The rendered number is stored server side in the session and the number submitted by the user is validated against this.
The image generated is a number with a wave distortion, image fuzzing with further noise added by including pre and post wave distortion lines.
This is the settings class I put together:
public class Settings
{
public Settings(string text)
{
Text = text;
Foreground = Color.Black;
Background = Color.White;
Width = 350;
Height = 100;
FontFamily = "Arial";
FontHeight = 60;
WaveAmplitude = 13;
WaveLength = 125;
FuzzAmount = 1;
LineCount = 6;
}
public string Text { get; set; }
public Color Foreground { get; set; }
public Color Background { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string FontFamily { get; set; }
public int FontHeight { get; set; }
public int WaveAmplitude { get; set; }
public int WaveLength { get; set; }
public int FuzzAmount { get; set; }
public int LineCount { get; set; }
}
Next I created a class which applies a sine wave transition along the Y axis based on the current position on the X axis. The amounts are controled by the
WaveAmplitude
and
WaveLength
settings.
public class WaveFunction
{
private Settings _settings;
public WaveFunction(Settings settings)
{
_settings = settings;
}
public int WaveAdjustment(int x)
{
double wl = (double)_settings.WaveLength;
double wa = (double)_settings.WaveAmplitude;
double dx = (double)x;
dx = dx % wl;
double d = dx / wl * 360;
return (int)Math.Round(Math.Sin(Radians(d)) * wa);
}
private double Radians(double degrees)
{
return Math.PI * degrees / 180.0;
}
}
Next I created the image drawer class:
public class ImageDrawer
{
Settings _settings;
public ImageDrawer(Settings settings)
{
_settings = settings;
}
}
Then I added the base image rendering method. This draws the text centred in the image. This uses the following settings to create the base image.
Width
,
Height
,
Foreground
,
Background
,
FontFamily
,
FontSize
private System.Drawing.Bitmap BaseImage()
{
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(_settings.Width, _settings.Height);
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
{
g.FillRectangle(new System.Drawing.SolidBrush(_settings.Background), new System.Drawing.Rectangle(0, 0, _settings.Width, _settings.Height));
System.Drawing.Font font = new System.Drawing.Font(_settings.FontFamily, _settings.FontHeight);
System.Drawing.SizeF textSizeF = g.MeasureString(_settings.Text, font);
System.Drawing.Size textSize = new System.Drawing.Size((int)Math.Round(textSizeF.Width), (int)Math.Round(textSizeF.Height));
System.Drawing.Point p = new System.Drawing.Point(_settings.Width / 2, _settings.Height / 2);
System.Drawing.Point dp = new System.Drawing.Point(p.X - (textSize.Width / 2), p.Y - (textSize.Height / 2));
System.Drawing.PointF dpF = new System.Drawing.PointF(dp.X, dp.Y);
g.DrawString(_settings.Text, font, new System.Drawing.SolidBrush(_settings.Foreground), dpF);
return bmp;
}
}
The next method adds a number of lines randomly to the image. The method uses the following settings:
Foreground
,
Width
,
Height
,
LineCount
private System.Drawing.Bitmap AddLines(System.Drawing.Bitmap flat)
{
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(flat))
{
Random r = new Random();
for (int i = 0; i < _settings.LineCount; i++)
{
int x = r.Next(0, _settings.Width);
int y = r.Next(0, _settings.Height);
int x2 = r.Next(0, _settings.Width);
int y2 = r.Next(0, _settings.Height);
System.Drawing.Point p1 = new System.Drawing.Point(x, y);
System.Drawing.Point p2 = new System.Drawing.Point(x2, y2);
g.DrawLine(new System.Drawing.Pen(_settings.Foreground), p1, p2);
}
}
return flat;
}
Now the method which adds the wave distortion. This method uses the
WaveFunction
class. It creates a new image with the background colour then for each pixel in the original image, uses the
WaveFunction
class to calculate it's vertical offset and where the offset position is within the bounds of the image it applies the pixel to the new image.
private System.Drawing.Bitmap AddWave(System.Drawing.Bitmap flat)
{
WaveFunction wf = new WaveFunction(_settings);
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(_settings.Width, _settings.Height);
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
{
g.FillRectangle(new System.Drawing.SolidBrush(_settings.Background), new System.Drawing.Rectangle(0, 0, _settings.Width, _settings.Height));
}
for (int x = 0; x < _settings.Width; x++)
for (int y = 0; y < _settings.Height; y++)
{
int newY = wf.WaveAdjustment(x);
if (y + newY >= 0 && y + newY < _settings.Height)
{
bmp.SetPixel(x, y + newY, flat.GetPixel(x, y));
}
}
return bmp;
}
The last method which changes the image is the fuzz method. This method randomly relocates each pixel of the image upto + or - number number of pixels stated in the
FuzzAmount
setting.
private System.Drawing.Bitmap AddFuzz(System.Drawing.Bitmap flat)
{
WaveFunction wf = new WaveFunction(_settings);
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(_settings.Width, _settings.Height);
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
{
g.FillRectangle(new System.Drawing.SolidBrush(_settings.Background), new System.Drawing.Rectangle(0, 0, _settings.Width, _settings.Height));
}
Random r = new Random();
for (int x = 0; x < _settings.Width; x++)
for (int y = 0; y < _settings.Height; y++)
{
int newX = x + (r.Next(-_settings.FuzzAmount, _settings.FuzzAmount));
int newY = y + (r.Next(-_settings.FuzzAmount, _settings.FuzzAmount));
if (newX >= 0 && newX < _settings.Width && newY >= 0 && newY < _settings.Height)
{
bmp.SetPixel(newX, newY, flat.GetPixel(x, y));
}
}
return bmp;
}
The only thing left to do was to tie all the various methods together:
public System.Drawing.Bitmap GetImage()
{
System.Drawing.Bitmap bmp = BaseImage();
bmp = AddLines(bmp);
bmp = AddWave(bmp);
bmp = AddLines(bmp);
bmp = AddFuzz(bmp);
return bmp;
}