Introduction
To reduce the incidence of spam and reduce the likelihood of denial of service attacks, many web pages include an image block with distorted 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 example, 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 sophisticated. 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 modifying 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))
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)
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
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)
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=
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 bitmap 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 Internet Explorer 7 and earlier will not display them. Internet Explorer 8 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 CodeProject web page thought it was malicious 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/