A few months ago, I scrounged around looking for a decent CAPTCHA script in PHP. I found several that worked OK, but none had all of the features I deemed necessary. In particular: (1) the size of the image should be variable; (2) the character set should be variable, and, if desired, include both upper and lower case characters; (2) the size of the characters within the image should be variable; and, (3) most importantly, each character should be randomly rotated AND offset (in both X and Y dimensions) within the image. The challenge, of course, is to stay ahead of the image recognition guys ... and they are pretty danged good! These days, for a few $$, you can clip out a CAPTCHA image and send it to, e.g., a certain website in Russia; usually, within a very few seconds, you will receive the code in clear text! Nasty business model, but for some, $$ are $$.
In any event, here's my recomposition of the best ideas out of what I downloaded:
Here's the entry point module:
<?php
// File Location: /tmp/CAPTCHA_1/form.php
// derived from the following:
// @author Edward Elliot
// @source http://www.sitepoint.com/article/toughen-forms-security-image
// @author Jeffrey Van Myers
// @copyright 2004-8 Blue By-ways, LLC
// @version 1.0
// @since 1.0
// @access public
session_start();
if (isset($_POST['submit']))
{
if ($_SESSION['security_code'] == $_POST['security_code'] && !empty($_SESSION['security_code']))
{
// Insert your code for processing the form here, e.g., emailing the submission, entering it into a database, etc.
echo 'You have successfully created an account';
unset($_SESSION['security_code']);
}
else
{
// Insert your code for showing an error message here
echo 'Sorry, you have provided an invalid security code';
}
}
else
{
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xHTML1/DTD/xHTML1-Transitional.DTD">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>CAPTCHA_1 Test</title>
</head>
<body>
<form action="form.php" method="post">
<img src="images.php" /><br />
<label for="security_code">Security Code: </label><input id="security_code" name="security_code" type="text" /><br />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>
<?php
}
?>
As you can see, it dynamically calls the main Class module:
<?php
// File Location: /tmp/CAPTCHA_1/images.php
// derived from the following:
// @author Edward Elliot
// @source http://www.sitepoint.com/article/toughen-forms-security-image
// @author Jeffrey Van Myers
// @copyright 2004-8 Blue By-ways, LLC
// @version 1.0
// @since 1.0
// @access public
session_start();
$oImage = new Images(); // instantiate object
if ($oImage->Create())
{
$sCode = $oImage->GetCode();
}
else
{
echo 'Image GIF library is not installed.';
}
exit; // end images.php
// Class to create CAPTCHA images
// @access public
class Images
{
var $_oImage; // image object
var $_oChar; // character object
var $_font = 'monofont.ttf'; // character font
var $_sLower = '23456789bcdfghjkmnpqrstvwxyz'; // lower case character set
var $_sUpper = '23456789bBcCdDfFgGhHjJkKmMnNpPqQrRsStTvVwWxXyYzZ'; // upper/lower case character set
var $_sPossible = ''; // set of possible characters, selected by _iCase
var $_iCase = 1; // _iCase: 0 => lower case only; 1 => upper and lower case
var $_iWidth = 150; // image width, in pxs
var $_iHeight = 40; // image heigth, in pxs
var $_iNumChars = 5; // number of characters in image
var $_iSize; // calculated image size
var $_iSpacing; // calculated character spacing
var $_sCode; // randomly generated CAPTCHA character string
var $_iColor = 1; // _iColor: 0 => black & white; 1 => RGB
// constructor: class Images
// @access public
function Images()
{
$this->_sPossible = $this->_iCase ? $this->_sUpper : $this->_sLower;
$this->_iSize = $this->_iHeight * 0.75; // font size will be 75% of the image height
$this->_oImage = imagecreate($this->_iWidth, $this->_iHeight);// create new image
$cBackground = imagecolorallocate($this->_oImage, 255, 255, 255); // allocate white background colour
$this->_iSpacing = (int)($this->_iWidth / $this->_iNumChars); // calculate spacing between characters based on width of image
} // end function Images
// function: create CAPTCHA image
// @access public
function Create()
{
if (!function_exists('imagegif')) // check for existence of GD GIF library
{
return false;
}
$this->_DrawNoise(); // draw random noise in image box
$this->_DrawCharacters(); // draw randomly selected/oriented CAPTCHA character string in image box
header('Content-type: image/gif'); // tell browser that data is gif
imagegif($this->_oImage); // write stream to browser
imagedestroy($this->_oImage); // free memory used in creating image
$_SESSION['security_code'] = $this->_sCode; // set CAPTCHA code
return true;
} // end function Create
// function: draw random noise in image box
// @access private
function _DrawNoise()
{
$iNoiseColor = imagecolorallocate($this->_oImage, 100, 120, 180);
for ($i = 0; $i < ($this->_iWidth * $this->_iHeight) / 3; $i++) // generate random dots in background
{
imagefilledellipse($this->_oImage, mt_rand(0, $this->_iWidth), mt_rand(0, $this->_iHeight), 1, 1, $iNoiseColor);
}
for ($i = 0; $i < ($this->_iWidth * $this->_iHeight) / 150; $i++) // generate random lines in background
{
imageline($this->_oImage, mt_rand(0, $this->_iWidth), mt_rand(0, $this->_iHeight), mt_rand(0, $this->_iWidth), mt_rand(0, $this->_iHeight), $iNoiseColor);
}
} // end function _DrawLines
// function: draw randomly selected/oriented CAPTCHA character string in image box
// @access private
function _DrawCharacters()
{
$this->_sCode = ''; // reset code
$iX = (int) $this->_iSpacing / 4; // calculate spacing in X dimension for 1st character
$iMidY = (int) $this->_iHeight / 2; // calculate mid-line of image box
for ($i = 0; $i < $this->_iNumChars; $i++) // loop through and write out selected number of characters
{
$sChar= substr($this->_sPossible, mt_rand(0, strlen($this->_sPossible)-1), 1); // select random character
$this->_sCode .= $sChar; // add to code string
if ($this->_iColor) // create RGB image
{
$iRandRed = (int) rand(0, 128); // select random red saturation
$iRandGreen = (int) rand(0, 128); // select random green saturation
$iRandBlue = (int) rand(0, 128); // select random blue saturation
$cTextColour = imagecolorallocate($this->_oImage, $iRandRed, $iRandGreen, $iRandBlue);
}
else // create gray scale image
{
$iRandGray = (int) rand(0, 128); // select random saturation
$cTextColour = imagecolorallocate($this->_oChar, $iRandGray, $iRandGray, $iRandGray);
}
$fRandRotate = (float) rand(0, 25); // select random positive rotation
$iRandDirection = (int) rand(0, 10); // select random rotation direction
if ($iRandDirection > 5)
{
$fRandRotate = (float) -$fRandRotate; // change to random negative rotation
}
$textbox = imagettfbbox($this->_iSize, $fRandRotate, $this->_font, $sChar) or die('Error in imagettfbbox function'); // create textbox and add text
$iY = ($this->_iHeight - $textbox[5]) / 2; // calculate spacing in Y dimension for this character
imagettftext($this->_oImage, $this->_iSize, $fRandRotate, $iX, $iY, $cTextColour, $this->_font , $sChar) or die('Error in imagettftext function');
$iX += $this->_iSpacing; // calculate spacing in X dimension for next character
}
} // end function _DrawCharacters
// function: return CAPTCHA character string
// @access public
function GetCode()
{
return $this->_sCode;
} // end function GetCode
} // end Class Images
?>
I've included extensive (for me) comments for ease of understanding & modification. Note that the code does check to make sure your server has the GD image-handling library installed; otherwise it pukes.
Even though this is set up to take the default values from the Class setup, it would, of course, be possible to pass them from the set-up code in the form.php module, or, if you want to play with fire, pass them as vars in the <img> URL (line 38 in form.php). If you do decide to use both upper and lower case characters, be sure to add an advisory line to the output (e.g., immediately after line 40 in form.php) to warn the client that they must enter the code using the appropriate case.
One enhancement I considered, but have not yet tackled, is to have multiple font sets and randomly select which set on a character-by-character basis. Should be relatively easy, but will require a bit of work.
I attached the monofont.ttf file as monofont.ttf.txt, because the uploader refused the file as .ttf. Hopefully, it will be OK when you strip off the ".txt" suffix.
My thanks to Edward Elliot (http://www.sitepoint.com/article/toughen-forms-security-image) for giving me such a good place to start.