2012-03-16

Captcha image generator with GdiPlusX

Recently I was navigating at Rick Strahl's Blog, when I found a very interesting post, "A Captcha Image generator for FoxPro", in which he shows how he created some captcha images. As Rick said in his post, "CAPTCHA basically displays a verification image next to a textbox that has to be filled out to validate the current request. It’s not a foolproof approach for validation and it has some issues with accessibility but it seems to be a common solution to this problem". The generated image contains a random text.

In that case, Rick chose to create a DLL in C++, and call it from VFP. Here, I'll show how to obtain the same results using the new GdiPlus-X library.

The codes below might be useful for other purposes too, because some interesting drawing techniques were used there.

To obtain the random string, I created a very simple function, that receives as parameter the quantity of characters that will be created:

PROCEDURE CreateString(tnLength)
   LOCAL lcText, lcChar, x, n
   lcText = ""
   FOR n = 1 TO tnLength
      x = INT(RAND() * 36)
      lcChar = IIF(x < 10, TRANSFORM(x), CHR(x + 55))
      lcText = lcText + lcChar
   ENDFOR
   RETURN lcText
ENDPROC

 

TIME TO PLAY!

 

IMPORTANT:

All samples below use the new GDIPlus-X library. Download the latest stable release from Codeplex:
http://www.codeplex.com/Wiki/View.aspx?ProjectName=VFPX&title=GDIPlusX

 

 

SAMPLE 1 - CREATE A SIMPLE IMAGE AND DRAW SOME TEXT

 captcha11 captcha12

** Captcha Image Generator
** Creates a very simple image containing a string
** Based on Rick Strahl post on his weblog
**
http://www.west-wind.com/wconnect/weblog/ShowEntry.blog?id=556

* The following sample demonstrates how to create a simple Captcha Generator

LOCAL lcText AS Character
lcText = CreateString(8)

DO LOCFILE("System.App")

LOCAL loBmp AS xfcBitmap
LOCAL loFont AS xfcFont
LOCAL loSizeF AS xfcSizeF
LOCAL loScreenGfx AS xfcGraphics
LOCAL loGfx AS xfcGraphics

WITH _SCREEN.System.Drawing
loScreenGfx = .Graphics.FromHwnd(_Screen.HWnd)
loFont = .Font.New("Tahoma",20)  && Font Name, Size

* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)

* Create the new bitmap
loBmp = .Bitmap.New(loSizeF.Width, loSizeF.Height)
* Obtain a Graphics object to draw
loGfx = .Graphics.FromImage(loBmp)
loGfx.Clear(.Color.White)

* Draw the whole String
loGfx.DrawString(lcText, loFont, .Brushes.Black, 0, 0)

* Save image to disk
loBmp.Save("c:\captcha1.png", .Imaging.ImageFormat.Png)
ENDWITH

* Show image in Explorer
RUN /n explorer.exe c:\captcha1.png
RETURN

PROCEDURE CreateString(tnLength)
   LOCAL lcText, lcChar, x, n
   lcText = ""
   FOR n = 1 TO tnLength
      x = INT(RAND() * 36)
      lcChar = IIF(x < 10, TRANSFORM(x), CHR(x + 55))
      lcText = lcText + lcChar
   ENDFOR
   RETURN lcText
ENDPROC

The above sample creates the most simple image, containing a rendom string inside. To get the size needed to create the Bitmap, in a first moment I created a Graphics object from the _Screen. This object has only one task: to allow us to call the function MeasureString to obtain the size needed for our bitmap, as you can see below:

loScreenGfx = .Graphics.FromHwnd(_Screen.HWnd)
loFont = .Font.New("Tahoma",20) && FontName, FontSize
* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)
* Create the new bitmap
loBmp = .Bitmap.New(loSizeF.Width, loSizeF.Height)

 

 

SAMPLE 2 - CREATE A SIMPLE IMAGE AND DRAW SOME TEXT ON A HATCH BACKGROUND

captcha21           captcha22        captcha23

** Captcha Image Generator
** Creates a very simple image containing a string on a hatch background
** Based on Rick Strahl post on his weblog
**
http://www.west-wind.com/wconnect/weblog/ShowEntry.blog?id=556

* The following sample demonstrates how to create a simple Captcha Generator

LOCAL lcText AS Character
lcText = CreateString(8)

DO LOCFILE("System.App")

LOCAL loBmp AS xfcBitmap
LOCAL loFont AS xfcFont
LOCAL loSizeF AS xfcSizeF
LOCAL loScreenGfx AS xfcGraphics
LOCAL loGfx AS xfcGraphics

WITH _SCREEN.System.Drawing
loScreenGfx = .Graphics.FromHwnd(_Screen.HWnd)
loFont = .Font.New("Tahoma",20)  && Font Name, Size

* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)

* Create the new bitmap
loBmp = .Bitmap.New(loSizeF.Width, loSizeF.Height)
* Obtain a Graphics object to draw
loGfx = .Graphics.FromImage(loBmp)
loGfx.Clear(.Color.White)

liRand = INT(RAND()*52) + 1

* Fill the background of the rectangle with a random Hatch Brush
loGfx.FillRectangle( ;
   .Drawing2D.HatchBrush.New(liRand, .Color.Black, .Color.White),;
   0,0,loBmp.Width,loBmp.Height)
* Draw the whole string
loGfx.DrawString(lcText, loFont, .Brushes.Black, 0, 0)

* Save image to disk
loBmp.Save("c:\captcha2.png", .Imaging.ImageFormat.Png)
ENDWITH

* Show image in Explorer
RUN /n explorer.exe c:\captcha2.png
RETURN

PROCEDURE CreateString(tnLength)
   LOCAL lcText, lcChar, x, n
   lcText = ""
   FOR n = 1 TO tnLength
      x = INT(RAND() * 36)
      lcChar = IIF(x < 10, TRANSFORM(x), CHR(x + 55))
      lcText = lcText + lcChar
   ENDFOR
   RETURN lcText
ENDPROC

This is exactly the same sample we used before, but adding a hatch background. Gdi+ provides 52 different preset patterns for hatch brushes. In this sample, I chose to use a random one. Test it, and choose the one that you like most:

* Fill the background of the rectangle with a random Hatch Brush
loGfx.FillRectangle( ;
   .Drawing2D.HatchBrush.New(liRand, .Color.Black, .Color.White),;
   0,0,loBmp.Width,loBmp.Height)

 

 

SAMPLE 3 - DRAW SOME TEXT ON A COLORED BACKGROUND USING RANDOM COLORS

captcha31 captcha32 captcha33

** Captcha Image Generator
** Creates a simple image containing a string
** Each character in a different random color
** in a random background color
** Based on Rick Strahl post on his weblog
**
http://www.west-wind.com/wconnect/weblog/ShowEntry.blog?id=556

* The following sample demonstrates how to create a simple Captcha Generator

LOCAL lcText AS Character
lcText = CreateString(8)

DO LOCFILE("System.App")

LOCAL loBmp AS xfcBitmap
LOCAL loFont AS xfcFont
LOCAL loSizeF AS xfcSizeF
LOCAL loScreenGfx AS xfcGraphics
LOCAL loGfx AS xfcGraphics

WITH _SCREEN.System.Drawing
loScreenGfx = .Graphics.FromHwnd(_Screen.HWnd)
loFont = .Font.New("Tahoma",20)  && Font Name, Size

* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)

* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)

LOCAL lnWidth, lnHeight, lnCharWidth
lnWidth = loSizeF.Width * 1.5 && Stretch 50% to ensure rotated
&& characters will fit

lnHeight = loSizeF.Height
lnCharWidth = lnWidth / LEN(lcText)

* Create the new bitmap
loBmp = .Bitmap.New(lnWidth, lnHeight)
* Obtain a Graphics object to draw
loGfx = .Graphics.FromImage(loBmp)
loGfx.Clear(.Color.White)

* Fill the background of the rectangle with a random Solid Brush color
* We need here a pastel color, so let's make each channel will be
* at least 180

* Draw the background rectangle
loGfx.FillRectangle( ;
   .SolidBrush.New(.Color.FromARGB(255, INT(RAND() * 76)+180, ;
   INT(RAND() * 76)+180, INT(RAND() * 76)+180)), ;
   0,0,loBmp.Width,loBmp.Height)
* Draw each character in a different random color
LOCAL n, lcChar
LOCAL loBrush AS xfcSolidBrush

FOR n = 0 TO LEN(lcText)
   lcChar = SUBSTR(lcText, n, 1)

   * Create a brush with a random color
   loBrush = .SolidBrush.New(.Color.FromRGB(INT(RAND() * 0xFFFFFF)))

   * Draw the character
   loGfx.DrawString(lcChar, loFont, loBrush, (n-1) * lnCharWidth, 0)
ENDFOR

* Save image to disk
loBmp.Save("c:\captcha3.png", .Imaging.ImageFormat.Png)
ENDWITH

* Show image in Explorer
RUN /n explorer.exe c:\captcha3.png
RETURN

PROCEDURE CreateString(tnLength)
   LOCAL lcText, lcChar, x, n
   lcText = ""
   FOR n = 1 TO tnLength
      x = INT(RAND() * 36)
      lcChar = IIF(x < 10, TRANSFORM(x), CHR(x + 55))
      lcText = lcText + lcChar
   ENDFOR
   RETURN lcText
ENDPROC

This time I chose to create the background using a Solid Brush with a random backColor. For that I wanted to ensure to obtain a pastel color, so I used

INT(RAND() * 76) + 180  for each RGB color channel. This way, each channel will have at least the value 180, ensuring to obtain a bright color.

To draw each color in a different random color was easy too. Just looped through the string and drawn each character in its proportional position. This time, I wanted to have ANY random color, so that could be any value between 0 (RGB[0,0,0] - black) and 0xFFFFFF (RGB[255,255,255] - white).

* Create a brush with a random color
loBrush = .SolidBrush.New(.Color.FromRGB(INT(RAND() * 0xFFFFFF)))

 

SAMPLE 4 - DRAW SOME TEXT ON A COLORED HATCH BACKGROUND USING RANDOM COLORS WITH CHARACTER ROTATION

captcha41 captcha42

captcha43 captcha44

** Captcha Image Generator
** Creates an image containing a string
** Each character in a different random color and a slight rotation
** Based on Rick Strahl post on his weblog
**
http://www.west-wind.com/wconnect/weblog/ShowEntry.blog?id=556

* The following sample demonstrates how to create a simple Captcha Generator

LOCAL lcText AS Character
lcText = CreateString(8)

DO LOCFILE("System.App")

LOCAL loBmp AS xfcBitmap
LOCAL loFont AS xfcFont
LOCAL loSizeF AS xfcSizeF
LOCAL loScreenGfx AS xfcGraphics
LOCAL loGfx AS xfcGraphics

WITH _SCREEN.System.Drawing
loScreenGfx = .Graphics.FromHwnd(_Screen.HWnd)
loFont = .Font.New("Tahoma",20)  && Font Name, Size

* Obtain the width and height needed for the string
loSizeF = loScreenGfx.MeasureString(lcText, loFont)

LOCAL lnWidth, lnHeight, lnCharWidth
lnWidth = loSizeF.Width * 1.5 && Stretch 50% to ensure rotated
&& characters will fit

lnHeight = loSizeF.Height
lnCharWidth = lnWidth / LEN(lcText)

* Create the new bitmap
loBmp = .Bitmap.New(lnWidth, lnHeight)
* Obtain a Graphics object to draw
loGfx = .Graphics.FromImage(loBmp)
loGfx.Clear(.Color.White)

* Fill the background rectangle using a Hatch Brush with random pastel colors
loGfx.FillRectangle( ;
.Drawing2D.HatchBrush.New(INT(RAND()*52), ;
.Color.FromARGB(255, INT(RAND() * 76)+180, INT(RAND() * 76)+180, INT(RAND() * 76)+180), ;
.Color.FromARGB(255, INT(RAND() * 76)+180, INT(RAND() * 76)+180, INT(RAND() * 76)+180), ;
0,0,loBmp.Width,loBmp.Height)

* Draw each character in a different random color
* and a random rotation angle from -40 to +40 degrees
LOCAL n, lcChar
LOCAL loBrush AS xfcSolidBrush
LOCAL loMatrix AS xfcMatrix

FOR n = 0 TO LEN(lcText)
   lcChar = SUBSTR(lcText, n, 1)

   * Create a brush with a random color
   loBrush = .SolidBrush.New(.Color.FromRGB(INT(RAND() * 0xFFFFFF)))

   * Create a Matrix to apply rotation to the characters
   loMatrix = _Screen.System.Drawing.Drawing2D.Matrix.New()

   * Calculate random angle
   lnAngle = INT(RAND() * 80) - 40
   * Rotate at the center of each character position
   loMatrix.RotateAt(lnAngle, ;
      .Point.New((n-1) * lnCharWidth + (lnCharWidth / 2), lnHeight / 2))

   * Associate the Matrix to the Graphics object
   loGfx.Transform = loMatrix

   * Draw the character
   loGfx.DrawString(lcChar, loFont, loBrush, (n-1) * lnCharWidth, 0)
ENDFOR

* Save image to disk
loBmp.Save("c:\captcha4.png", .Imaging.ImageFormat.Png)
ENDWITH

* Show image in Explorer
RUN /n explorer.exe c:\captcha4.png
RETURN

PROCEDURE CreateString(tnLength)
   LOCAL lcText, lcChar, x, n
   lcText = ""
   FOR n = 1 TO tnLength
      x = INT(RAND() * 36)
      lcChar = IIF(x < 10, TRANSFORM(x), CHR(x + 55))
      lcText = lcText + lcChar
   ENDFOR
   RETURN lcText
ENDPROC

This time we did the same, but using a random Hatch Brush for the background, and aplying a slight rotation on each character. String rotation was already discussed in a previous post, anyway, here's a short explanation:

"System.Drawing" brings a method that permits to draw any object (shape, text, another image) using rotation, at any angle. For that we need to create a Matrix object, and use the "RotateAt" method, passing the center point of the object to be rotated. In this case, I needed to pass the center of the string.

* Create a Matrix to apply rotation to the characters
loMatrix = .Drawing2D.Matrix.New()

* Calculate random angle
lnAngle = INT(RAND() * 80) - 40
* Rotate at the center of each character position
loMatrix.RotateAt(lnAngle, ;
   .Point.New((n-1) * lnCharWidth + (lnCharWidth / 2), lnHeight / 2))

* Associate the Matrix to the Graphics object
loGfx.Transform = loMatrix

* Draw the character
loGfx.DrawString(lcChar, loFont, loBrush, (n-1) * lnCharWidth, 0)

 

WHAT MORE?

captcha51

We can do some more things, like to draw some random lines on the image. One way could be just adding this simple code immediately before saving the image:

LOCAL x1, y1, x2, y2, lnColor
FOR n = 1 TO 10
   x1 = RAND() * lnWidth
   x2 = RAND() * lnWidth
   y1 = RAND() * lnHeight
   y2 = RAND() * lnHeight
   lnColor = RAND() * 0xFFFFFF
   loGfx.DrawLine(.Pen.New(.Color.FromRGB(lnColor),1),x1, y1, x2, y2)
ENDFOR

Although it is known that there are already some Captcha decoders, this might be useful for some situations. My goal in this post was just to show some techniques to create good captchas. There's a lot to be improved. You can use random and irregular fonts, other backgrounds, apply some transformations to characters, like scaling, shearing, blurring, halo effects. All this with GDI+ !

In the next release of the library, I hope to send some samples that create some interesting effects on texts, like the ones below, that might be useful for this too.

             cooltext

4 comments:

  1. this code works fine, but on a hidden from return a black image, what are I missing?
    local locapturebmp as xfcbitmap
    do locfile("system.app")
    with _screen.system.drawing
    locapturebmp = .bitmap.fromscreen(_screen.hwnd)
    locapturebmp.save("z.png", .imaging.imageformat.png)
    ENDWITH

    ReplyDelete
  2. correction, this code works fine, but on a hidden FORM return a black image, what are I missing?
    local locapturebmp as xfcbitmap

    ReplyDelete
  3. Nothing is missing. To capture the image, the form MUST be visible!

    ReplyDelete
  4. Hi Daniel,
    To capture the image of the form, it MUST be visible !

    ReplyDelete