Adventures in creating and destroying sounds
RSS icon Home icon
  • CAPTCHA alternative using random photos in PHP

    Posted on August 1st, 2010 Bob No comments

    New CAPTCHA

    I administer a music related forum and registrations from ‘bots’ were getting a little out of hand. On the forum’s registration page, I was using a CAPTCHA scheme that I made a very long time ago. Today I was reading up on how easily OCR technology can crack CAPTCHAs found on forms all over the Web. I realized that perhaps that this was one reason why I was getting so many spammy registrations. I thought that my CAPTCHA may need some updating so I came up with an entirely new scheme.

    The basic idea is to present the user with a group of images of items with random numbers superimposed over each of the images.

    Old CAPTCHA

    The user has to enter the number that corresponds to one of the randomly chosen images to authenticate. Hopefully, a machine will not be able to discern a ‘boat’ from a ‘dog’.

    It turned out to be relatively simple to implement. First, I grabbed a bunch of images of ‘things’… a bird, a dog, an umbrella, a banana, a car, a cat, etc. from around the Web. I wrote a PHP class that generates a composite image from 9 smaller ones and derives a 3 digit code and the name of the thing in the photo.

    So, here’s the code I came up with:

    <?php
    
    // Written by Bob Scott
    // http://www.negatron.org/
    
    class confirmationImage {
    
    	// Class constructor
    	function confirmationImage() {
    		$this->photoDir = './things/'; // with trailing slash - directory where the images are stored
    		$this->textAngle = 25;
    		$this->textPosX = 56;
    		$this->textPosY = 96;
    		$this->textFontSize = 20;
    		$this->randFont = './fnt/arialbi.ttf'; // True Type font to use
    		$this->writeDir = $_SERVER['DOCUMENT_ROOT'] . '/cImages'; // directory must be readable, writable and executable by web server user
    		$this->writeURL = '/cImages';
    	}
    
    	function makeImage() {
    
    		$randomNumbers = array();
    
    		// Get all available photos
    		if ($handle = opendir($this->photoDir)) {
    		   while (false !== ($file = readdir($handle))) {
    		       if ($file != "." && $file != ".." && !strstr($file,'php')) {
    		           $photos[] = $this->photoDir . $file;
    		       }
    		   }
    		   closedir($handle);
    		}
    
    		// Randomize photos
    		shuffle($photos);
    
    		// Get the first 9 random images.
    		// Create a 3 digit number for each of the images
    		// also derive the name of the image from the file name
    
    		for ($i = 1 ; $i <= 9 ; $i++) {
    			$im[$i] = @imagecreatefrompng($photos[$i]);
    			$fontColor[$i] = imagecolorallocate($im[$i], rand(188,255), rand(188,255), rand(188,255));
    			$fontColorB[$i] = imagecolorallocate($im[$i], rand(0,120), rand(0,120), rand(0,120));
    			$rNum = rand(100,999);
    			// never have duplicate random numbers
    			while (!in_array($rNum, $randomNumbers)) {
    				$randomNumbers[$i] = $rNum;
    				$rNum = rand(100,999);
    			}
    			// get photo name from file name
    			$photoNames[$i] = strtoupper(str_replace('.png', '', str_replace($this->photoDir, '', $photos[$i])));
    			// put numbers on each photo
    			imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX-1, $this->textPosY-1, $fontColor[$i], $this->randFont, $randomNumbers[$i]);
    			imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX+1, $this->textPosY+1, $fontColor[$i], $this->randFont, $randomNumbers[$i]);
    			imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX-2, $this->textPosY-2, $fontColor[$i], $this->randFont, $randomNumbers[$i]);
    			imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX+2, $this->textPosY+2, $fontColor[$i], $this->randFont, $randomNumbers[$i]);
    			imagettftext($im[$i], $this->textFontSize, $this->textAngle, $this->textPosX, $this->textPosY, $fontColorB[$i], $this->randFont, $randomNumbers[$i]);
    		}
    
    		// Make final composite image
    		$imf = imagecreatetruecolor(300, 300);
    		$cc = 1;
    		for ($x = 0 ; $x < 300 ; $x += 100) {
    			for ($y = 0 ; $y < 300 ; $y += 100) {
    				imagecopy ($imf, $im[$cc], $x, $y, 0, 0, 100, 100);
    				$cc++;
    			}
    		}
    
    		// What number / photo combination shall we use??
    		$crn = rand(1,9);
    		$this->photo_name = $photoNames[$crn];
    		$this->photo_number = $randomNumbers[$crn];
    
    		$ffn = uniqid('cpt_') . '.png';
    
    		$fFile = $this->writeDir . '/' . $ffn;
    		$this->fURL = $this->writeURL . '/' . $ffn;
    
    		imagepng ($imf, $fFile);
    		imagedestroy($imf);
    		for ($i = 1 ; $i <= 9 ; $i++) {
    			imagedestroy($im[$i]);
    		}
    
    		return TRUE;
    
    	}
    
    }
    
    ?>

    To access this class, just do something like this in your PHP script that generates your registration page:

    // make confirmation string
    include_once('inc_class_image_confirmation.php');
    $cm = new confirmationImage;
    $cm->makeImage();
    $code = $cm->photo_number;
    $confirm_image = '<img src="' . $cm->fURL . '" alt="" />';
    $picName = $cm->photo_name;

    You will want to store the value of the variable $code on the backend… either in a cookie variable, a session variable or even a database for later access. You will the want to display $confirm_image and $picName to the user. The user’s input must match $code when error checking your registration page.

    When setting up everything, make a directory that is readable, writable and executable by the Web server user toy hold the confirmation images. On *nix machines, a simple

    chmod 777 cImages

    should suffice. I made the individual images all PNG format and resized them to 100×100 pixels. Name the individual files with the name of the object in the photo. For example, the photo of the bird is ‘bird.png’. I purposely choose photos of items that have one word in their name… though ‘rubber ducky’ was awfully tempting. Also, your PHP installation must have GD with PNG support built in or this will not work.

    Here’s a directory listing of the images I wound up choosing:

    $ ls -1
    ball.png
    banana.png
    bird.png
    boat.png
    bread.png
    car.png
    cat.png
    clock.png
    cloud.png
    computer.png
    cup.png
    dog.png
    drum.png
    flower.png
    guitar.png
    hat.png
    key.png
    knife.png
    lamp.png
    pencil.png
    phone.png
    shirt.png
    shoe.png
    spoon.png
    tree.png
    umbrella.png

    A few thoughts while building this. I realize that users that are visually impaired will not be able to use this CAPTCHA scheme. If you decide to implement something like this for your Web site, please take note of this important fact. I really tried to make all the items in the photos as simple as possible in hopes that non-native English speakers will still be able to use the scheme with a super basic understanding of English.  My forum is in English anyways.

    Also, the directory with the confirmation images will fill up over time. You might want to run a cron job that cleans those up from time to time:

    # remove stale confirmation images
    0 20 * * * /usr/bin/find /usr/local/apache/htdocs/cImages -type f -mmin +360 -exec rm -rf {} \; >/dev/null 2>&1

    This sort of CAPTCHA alternative has possibly been done before but this is my take on it. Let me know what you think!

    Leave a reply

    *
    To help curb automated spam-bots. Click on the picture to hear audio of the word.
    Click to hear an audio file of the anti-spam word