Pretty CAPTCHA Creative Commons License

posted terribly early in the morning by Constantinos. Filed under Code

This post was originally published in 2007
The tips and techniques explained may be outdated.

I know, I know… CAPTCHA is outdated. But there are still cases where it might be useful, and it’s hard to make one that’s good looking, easy to read by humans and has a certain level of complexity for machines. This is what led me to create my own PHP version, that can be pretty much plugged in to anything. I was meaning to make this into a WordPress plugin, but meh…

This script uses the $_SESSION variable to store the generated pass string. Therefore, to validate the CAPTCHA, you need this little code inserted in the page that handles the form post:


$captcha = $_POST['captcha'];
session_start();
if (($captcha != $_SESSION['captcha']) || !$captcha) {
    // Verification failed... Do what you will.
}
unset($_SESSION['captcha']);

That’s it on that end. If you need help with that, and I have time, I might offer some assistance. Did not really check on how that could be implemented for WordPress, and therefore don’t (yet) know if it’s easy or not.

Now on to the main course. The script is at the end of this post. To use it, simply save it as captcha.php, place this font file (or any other ttf font you want) in the same directory with it, and then place this image link anywhere in your code:

<span><img src='captcha.php' alt='Captcha image' id='captcha_img' /></span>

That’s it. A nice looking CAPTCHA for your site 🙂

If you want to add a link to refresh the image in-place, you can use this javascript (doesn’t work in Safari):

<a href='#' onclick="var i = document.getElementById('captcha_img'); var n = new Image(); var rand = (Math.round((Math.random()*1000)+1)); n.onload = function() { i.parentNode.appendChild(n); i.parentNode.removeChild(i); n.id='captcha_img'; }; n.src='captcha.php?'+rand; return false;">reload image</a>

This piece of code will reload the image and reset the stored pass word.

A preview of the image produced is after the jump.

The CAPTCHA code, released under Creative Commons Attribution-Noncommercial-Share Alike 3.0 License

<?php
/**
 * Generate a CAPTCHA and display the image
 *
 * @author Constantinos Neophytou
 * @link http://www.cneophytou.com/
 * @license Creative Commons Attribution-Noncommercial-Share Alike 3.0  License
 *     {@link http://creativecommons.org/licenses/by-nc-sa/3.0/}
 **/
 
/**
 * Initialize page
 **/
 
session_start();
 
header("Content-Type: image/png");
header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
 
create_image();
exit;
 
/**
 * Generate a random string, and create a CAPTCHA image out of it
 */
function create_image()
{
    // generate pronouncable pass
    $pass = genPassword(5, 6);
    $font = './captcha.ttf';
    $maxsize = 50;
    $sizeVar = 25;
    $rotate = 20;
    $bgcol = 50; // + 50
    $bgtextcol = 80; // + 50
    $textcol = 205; // + 50
 
    // remember the pass
    $_SESSION["captcha"] = $pass;
 
    // calculate dimentions required for pass
    $box = @imageTTFBbox($maxsize,0,$font,$pass);
    $minwidth = abs($box[4] - $box[0]);
    $minheight = abs($box[5] - $box[1]);
 
    // allow spacing for rotating letters
    $width = $minwidth + 100;
    $height = $minheight + rand(5,15); // give some air for the letters to breathe
    // create initial image
    $image = ImageCreatetruecolor($width, $height);  
 
    if (function_exists('imageantialias')) {
        imageantialias($image, true);
    }
    // define background color - never the same, close to black
    $clr_black = ImageColorAllocate($image, rand($bgcol, $bgcol+30),
                                    rand($bgcol, $bgcol+30), rand($bgcol, $bgcol+30));
    imagefill($image, 0, 0, $clr_black);
 
    // calculate starting positions for letters
    $x = rand(10, 25);//($width / 2) - ($minwidth / 2);
    $xinit = $x;
    $y = ($minheight-abs($box[1])) + (($height - $minheight) / 2);
 
    // fill the background with big letters, colored a bit lightly, to vary the bg.
    $bgx = $x / 2;
    $size = rand($maxsize - 10, $maxsize);
    for($i = 0; $i < strlen($pass); $i++) {
        // modify color a bit
        $clr_white = ImageColorAllocate($image, rand($bgtextcol, $bgtextcol+50),
                    rand($bgtextcol, $bgtextcol+50), rand($bgtextcol, $bgtextcol+50));
        $angle = rand(0-$rotate, $rotate);
        $letter = substr($pass, $i, 1);
        imagettftext($image, $size*2, $angle, $bgx, $y, $clr_white, $font, $letter);
        list($x1, $a, $a, $a, $x2) = @imageTTFBbox($size,$angle,$font,$letter);
        $bgx += abs($x2 - $x1);
    }
 
    // for each letter, decide a color, decide a rotation, put it on the image,
    //     and figure out width to place next letter correctly
    for($i = 0; $i < strlen($pass); $i++) {
        // modify color a bit
        $clr_white = ImageColorAllocate($image, rand($textcol, $textcol+50), 
                            rand($textcol, $textcol+50), rand($textcol, $textcol+50));
 
        $angle = rand(0-$rotate, $rotate);
        $letter = substr($pass, $i, 1);
        $size = rand($maxsize - $sizeVar, $maxsize);
        $tempbox = @imageTTFBbox($size,$angle,$font,$letter);
 
        $y = (abs($tempbox[5] - $tempbox[1])) +
                                        (($height - abs($tempbox[5] - $tempbox[1])) / 2);
 
        imagettftext($image, $size, $angle, $x, $y, $clr_white, $font, $letter);
        $x += abs($tempbox[4]-$tempbox[0]);
    }
    // figure out final width (same space at the end as there was at the beginning)
    $width = $xinit + $x;
 
    // throw in some lines
    $clr_white = ImageColorAllocate($image, rand(160, 200), rand(160, 200),
                                             rand(160, 200));
    imagelinethick($image, rand(0, 10), rand(0, $height / 2), rand($width - 10, $width),
                           rand($height / 2, $height), $clr_white, rand(1, 2));
    $clr_white = ImageColorAllocate($image, rand(160, 200), rand(160, 200),
                                             rand(160, 200));
    imagelinethick($image, rand(($width / 2) - 10, $width / 2),
                            rand($height / 2, $height), rand(($width / 2)+ 10, $width),
                           rand(0, ($height / 2)), $clr_white, rand(1, 2));
 
    // generate final image by cropping initial image to the proper width,
    //     which we didn't know till now.
    $finalimage = ImageCreatetruecolor($width, $height);
    if (function_exists('imageantialias')) {
        imageantialias($finalimage, true);
    }
    imagecopy($finalimage, $image, 0, 0, 0, 0, $width, $height);
    // clear some memory
    imagedestroy($image);
 
    // dump image
    imagepng($finalimage);
 
    // clear some more memory
    imagedestroy($finalimage);
}
 
/**
 * Draw lines through an image
 * @param resource
 * @param int
 * @param int
 * @param int
 * @param int
 * @param int - the color to use for the line
 * @param int - line thickness
 * @return bool - true on success
 */
function imagelinethick($image, $x1, $y1, $x2, $y2, $color, $thick = 1)  {
   if ($thick == 1) {
       return imageline($image, $x1, $y1, $x2, $y2, $color);
   }
   $t = $thick / 2 - 0.5;
   if ($x1 == $x2 || $y1 == $y2) {
       return imagefilledrectangle($image, round(min($x1, $x2) - $t),
                                           round(min($y1, $y2) - $t),
                                           round(max($x1, $x2) + $t),
                                           round(max($y1, $y2) + $t),
                                   $color);
   }
   $k = ($y2 - $y1) / ($x2 - $x1); //y = kx + q
   $a = $t / sqrt(1 + pow($k, 2));
   $points = array(
       round($x1 - (1+$k)*$a), round($y1 + (1-$k)*$a),
       round($x1 - (1-$k)*$a), round($y1 - (1+$k)*$a),
       round($x2 + (1+$k)*$a), round($y2 - (1-$k)*$a),
       round($x2 + (1-$k)*$a), round($y2 + (1+$k)*$a),
   );    
   imagefilledpolygon($image, $points, 4, $color);
   return imagepolygon($image, $points, 4, $color);
}
 
/**
 * Generate a random, pronouncable password
 * Modified to exclude letters which don't show up well in the CAPTCHA
 * @link http://www.zend.com/code/codex.php?id=215&single=1
 * @author Rival7 {@link http://www.zend.com/code/search_code_author.php?author=Rival7}
 * @author Constantinos Neophytou
 */
function genPassword($minlen, $maxlen) {
    srand((double)microtime()*1000000);
 
    $vowels = array('a', 'e', 'i', 'o', 'u');
    $cons = array('b', 'c', 'd', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'r', 's', 't', 'v',
     'w', 'tr', 'cr', 'br', 'fr', 'th', 'dr', 'ch', 'ph', 'wr', 'st', 'sp', 'sw', 'pr');
    // removed 'l', 'sl', 'cl'
 
    $num_vowels = count($vowels);
    $num_cons = count($cons);
 
    $length = rand($minlen, $maxlen);
 
    $start = rand(0, 1);
    if ($start) {
        $first = $cons;
        $num_first = $num_cons;
        $second = $vowels;
        $num_second = $num_vowels;
    } else {
        $first = $vowels;
        $num_first = $num_vowels;
        $second = $cons;
        $num_second = $num_cons;
    }
 
    for($i = 0; $i < $length; $i++) {
        $add = $first[rand(0, $num_first - 1)] . $second[rand(0, $num_second - 1)];
        $password .= $add;
        $i += (strlen($add) - 1);
    }
 
    //$password = substr($password, 0, $length);
    return $password;
}
 
?>

Download this script

The produced image:
(if you know any coding whatsoever, you can easily change the thickness of the lines that run through the image)

Captcha image
reload image

6 Responses to “Pretty CAPTCHA”

  1. 1. Comment by Constantinos
    on 6 Mar 2007 @ 10:47 pm

    As an addition to the post, I’d just like to mention that what I was looking for from a captcha is:

    1. No processing should happen on the client side
    2. It should not be text based or hidden field based
    3. Should not be readable by a plain OCR program
    4. Have at least some level of complexity for image readers
    5. Be customized, and look nice

    Now obviously this is not going to start appearing on any sites like digg.com, and I’ve only implemented it on a site that won’t worth any spammers time to write an image processing script that reads this image. The code chooses the colors randomly, and one suggestion for improvement I received was to add some basic shapes around the letters that were the same color as the letter. That’s a pretty good idea and easy to implement, and I might just do that if I can come up with a way that will guarantee the image will always (or most of the time) be readable by humans…

  2. 2. Pingback by James Cooke dot info » Blog Archive » Form spam - PHP CAPTCHAs and Akismet
    on 27 May 2007 @ 3:42 pm

    […] Constantinos Neophytou’s Pretty CAPTCHA This option looks excellent, although I didn’t give it a go for two reasons – I couldn’t see how to “attribute the work in the manner specified by the author” in order to comply with the CC license that the code is released under, and in looking at the contact form at the bottom of the page I found reCAPTCHA, which distracted me completely! […]

  3. 3. Comment by Alix Axel
    on 1 Jul 2007 @ 7:50 pm

    Very nice code indeed!

  4. 4. Comment by Angelivene
    on 21 May 2008 @ 10:12 pm

    The image “http://localhost/captcha/captcha.php” cannot be displayed, because it contains errors.

    this is what i get when running the script on my localhost, all GD functions are working on my php conf

  5. 5. Comment by Constantinos
    on 22 May 2008 @ 1:01 am

    [quote post=”87″]this is what i get when running the script on my localhost, all GD functions are working on my php conf[/quote]

    Comment out the header("Content-Type: image/png"); line, and visit the script directly (i.e. load http://localhost/captcha/captcha.php in your browser). That should give you the exact errors that are produced, and help you debug it.

  6. 6. Comment by doc
    on 3 Jun 2009 @ 11:00 am

    you should put the letters, on average, a little closer together, so that it prevents scanners from easily ignoring your line. overlapping letters are very hard for amateur OCR’s to parse. also, the font should be randomized (catpcha1.ttf, catpcha2.ttf). should have 50 fonts at a minimum. finally, you need to throttle the number of images generates on a per-ip or per-cookie basis…. or else someone will crack it using a random-seed attack or partial-brute-force with an OCR, etc.