Introduction

To reduce the incidence of spam and reduce the likelihood of denial of service attacks, many web pages include an image block with distored text, known as a Captcha. (There is a good article on Wikipedia at http://en.wikipedia.org/wiki/CAPTCHA).

The Code Project and other open sources have a number of code snippets for producing a captcha image on a web page. However, the majority are old and use moderately complex javascript and/or C#. The code in this article using visual basic and the increasing power available in the .NET framework to produce significantly more complex and simple code.  For exampale, the 30 lines needed in the past to convert binary data to base64 can now be done in a single line. 

The image is drawn by obtaining a graphics object from the in memory bit map.  Again the .NET framework has simplified the process of transforming the bitmap to a form that can be displayed on a webpage with the bitmap.save(stream, imageformat) function to write the image to a memory stream.  The memory stream is then read and converted to the required base64 string.

The captcha is not particularly sophistocated.  However, it would be easy to modify the code to have, for example, smoother angle and size changes and have a more (human) attractive disruptive line across the text. 

Using the code

The code is a short class which has a method for generating a fake word and a second method for producing an in-memory bitmap with the distorted text. Having produced these, the fake word is place in a hidden text box on the page and the image is added to the page by modifiying the src attribute of an <img /> tag placed on the page.

(I wish to acknowledge www.tipstricks.org for the basis of the fake word generation - written in script but now translated into VB.NET. The RndInterval function could have been updated, but it was not worth doing so).

       
Imports System.Drawing

Public Class Captcha

Dim bmpCaptcha As Bitmap

Dim iBMPHeight As Integer = 50

Dim iBMPWidth As Integer = 220

Dim sLeftMargin As Single = 20

Dim sTopMargin As Single = 10

Dim g As Graphics

Dim sWord As String

Dim sLetter As String

Dim sfLetter As SizeF

Dim rfLetter As RectangleF

Dim sX1 As Single = 0

Dim sY1 As Single = 0

Dim sX2 As Single = 0

Dim sY2 As Single = 0

Dim sTemp As Single

Dim iAngle As Integer

Dim sOffset As Single



Public Function GenerateCaptcha(ByVal sWord As String) As Bitmap

Dim ixr As Integer

bmpCaptcha = New Bitmap(iBMPWidth, iBMPHeight, Drawing.Imaging.PixelFormat.Format16bppRgb555)

g = Graphics.FromImage(bmpCaptcha)

Dim drawBackground As New SolidBrush(Color.Silver)

g.FillRectangle(drawBackground, New Rectangle(0, 0, iBMPWidth, iBMPHeight))



' Create font and brush.

Dim drawFont As New Font("Times New Roman", 20)

Dim drawBrush As New SolidBrush(Color.DarkBlue)

Dim strFormat As New StringFormat(StringFormatFlags.FitBlackBox)

sfLetter = New SizeF(30, 30)

' Draw string to screen.

For ixr = 0 To sWord.Length - 1

sLetter = sWord.Substring(ixr, 1)

g = Graphics.FromImage(bmpCaptcha)

rfLetter = New RectangleF(sLeftMargin, sTopMargin, 30, 30)

sfLetter = g.MeasureString(sLetter, drawFont, sfLetter, strFormat)

iAngle = RndInterval(0, 20) - 10

With rfLetter

sOffset = sLeftMargin * Math.Tan(iAngle * Math.PI / 180)

.Y = sTopMargin - sOffset

.Width = sfLetter.Width + 10

.Height = sfLetter.Height

End With

g.RotateTransform(iAngle)

g.DrawString(sLetter, drawFont, drawBrush, rfLetter)

sLeftMargin += sfLetter.Width + 2

Next

Dim drawPen As Pen = New Pen(Color.Crimson, 1)

For ixr = 0 To 3

sX1 = sX2

Do While Math.Abs(sX1 - sX2) < iBMPWidth * 0.5

sX1 = RndInterval(2, iBMPWidth - 2)

sX2 = RndInterval(2, iBMPWidth - 2)

Loop

sY1 = sY2

Do While Math.Abs(sY1 - sY2) < iBMPHeight * 0.5

sY1 = RndInterval(2, iBMPHeight - 2)

sY2 = RndInterval(2, iBMPHeight - 2)

Loop

If RndInterval(0, 2) > 1 Then

sTemp = sX1

sX1 = sX2

sX2 = sTemp

End If

If RndInterval(0, 2) > 1 Then

sTemp = sY1

sY1 = sY2

sY2 = sTemp

End If



g.DrawLine(drawPen, sX1, sY1, sX2, sY2)

Next

g.Dispose()

Return bmpCaptcha

End Function

Public Function GenerateFakeWord(ByVal iLengthRequired As Integer) As String

Dim sVowels As String = "AEIOU"

Dim sConsonants As String = "BCDFGHJKLMNPQRSTVWXYZ"

Dim iNbrVowels As Integer

Dim iNbrConsonants As Integer

Dim sWord As String

Dim bUseVowel As Boolean

Dim iWordLength As Integer

Dim sPattern As String

iNbrVowels = 0

iNbrConsonants = 0

bUseVowel = False

sWord = ""

Randomize()

For iWordLength = 1 To iLengthRequired

If (iWordLength = 2) Or ((iLengthRequired > 1) And (iWordLength = iLengthRequired)) Then

bUseVowel = Not bUseVowel

ElseIf (iNbrVowels < 2) And (iNbrConsonants < 2) Then

bUseVowel = ((Rnd(1) * 2) > 1)

ElseIf (iNbrVowels < 2) Then

bUseVowel = True

ElseIf (iNbrConsonants < 2) Then

bUseVowel = False

End If



sPattern = IIf(bUseVowel, sVowels, sConsonants)

sWord = sWord & sPattern.Substring(Int(Rnd(1) * sPattern.Length), 1)

If bUseVowel Then

iNbrVowels = iNbrVowels + 1

iNbrConsonants = 0

Else

iNbrVowels = 0

iNbrConsonants = iNbrConsonants + 1

End If

Next

Return sWord

End Function

Private Function RndInterval(ByVal iMin As Integer, ByVal iMax As Integer) As Integer

Randomize()

Return Int(((iMax - iMin + 1) * Rnd()) + iMin)

End Function

End Class

To use class, include something like the following three tags on the aspx page:

<body onload="onload()">


<asp:Image ID="imgCaptcha" runat="server" style="z-index: 1; left: 570px; top: 220px; 
position: absolute; width: 220px; height: 50px;" /> 

<asp:TextBox ID="txtCaptcha" runat="server" 
style="z-index: 1; left: 67px; top: 474px; position: absolute" 
Visible="False">txtCaptcha</asp:TextBox>


And, finally, you will need some code in the aspx.vb code page to generate the captcha image and display it on the web page. The variable bOutputCaptcha can be set to output the captcha image or have the page OnLoad function do nothing and hide the image control.  Typically the page will be reloaded after the user's response which can be compared with the word which is stored in the hidden textbox, txtCaptcha.  (The viewstate of aspx controls is said to be sufficiently secure that it is unlikely a computer can extract the value easily.  However, the value could be left server side if you really really thought this necessary).

 
Private Sub ForgottenPassword_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender

        ' Example of inline base64 image tag: <img width="16" height="14" alt="embedded folder icon" src="data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7" />

        Dim sWord As String
        Dim bmpCaptcha As Bitmap
        Dim sScript As String
        Dim sImage64 As String
        Dim iLength As Integer
        Dim iRead As Integer
        Dim msCaptcha As MemoryStream

        If bOutputCaptcha Then

              sWord = myCaptcha.GenerateFakeWord(6)
              txtCaptcha.Text = sWord

              bmpCaptcha = myCaptcha.GenerateCaptcha(sWord)

              '   The following commented out code can be used for testing
              '   It writes the image to a page by itself
             'Response.ContentType = "image/jpeg"
             'Response.AppendHeader("Content-Disposition", "inline; filename=captcha.bmp")
             'Response.CacheControl = "no-cache"
             'Response.AppendHeader("Pragma", "no-cache")
             'Response.Expires = -1
             'bmpCaptcha.Save(Response.OutputStream, Imaging.ImageFormat.Jpeg)
             'Response.Flush()

             msCaptcha = New MemoryStream
             bmpCaptcha.Save(msCaptcha, Imaging.ImageFormat.Jpeg)

             sImage64 = ""

             iLength = msCaptcha.Length
             Dim byteImage As Byte()

             byteImage = New Byte(CType(msCaptcha.Length, Integer)) {}

             msCaptcha.Position = 0
             iRead = msCaptcha.Read(byteImage, 0, iLength)
             sImage64 = Convert.ToBase64String(byteImage)

             sScript = vbCr + <script language="Javascript"> " vbCr

             sScript += "function onload() {" + vbCr
             sScript += "var image;" + vbCr
             sScript += "image=document.getElementById(""imgCaptcha"");" + vbCr
             sScript += "image.src='data:image/gif;base64," + sImage64 + "'"
            
             '   It would be possible to test for IE7 or earlier here and put the captcha word into the alt text - but 
             '   does increase the chances of a denial of service attack if someone figures this out
             sScript += "}" + vbCr + vbCr
             sScript+="</Script>" + vbCr

             Me.ClientScript.RegisterClientScriptBlock(Me.GetType(), quot;image", sScript)

             msCaptcha.Close()
             bmpCaptcha.Dispose()

        Else

             imgCaptcha.Visible = False
             sScript = vbCr + <script language="Javascript"> " vbCr
             sScript += "function onload() {" + vbCr
             sScript += "}" + vbCr + vbCr
             sScript+="</Script>" + vbCr

             Me.ClientScript.RegisterClientScriptBlock(Me.GetType(), "image", sScript)

        End If

    End Sub

Note the little bit of code that is commented out - it is possible to save the bitmpa directly to the http response stream and display the image alone on the page for testing purposes.

The user will need to have scripting permitted on their browser.

The image uses the inline URI for image display. The image is encoded in base64 (i.e. 7 bit characters). This means the image is typically 30% larger than the original bit map (which  in this example is typically about 3kb).

Not all browsers will display inline base64 images - most importantly IE 7 and earlier will not display them. IE8 and later will display the images and have a limited of 32kb. Firefox and Safari are said to have done so for much longer, but many are limited to 4kb images. The bitmap images produced here are typically 3kb so that even after the conversion to base64 should be able to be displayed.

(I hope the script is accurately translated.  The Code Project web page thought it was malacious code and removed everything between the <script></script> tags - so I could not copy it directly and had to type the code between separately!!)

History

No changes have been made to the code at this stage (Jan 2011).

' http://www.tipstricks.org/

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架