CAPTCHA alternative using random photos in PHP

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
// //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

Your email address will not be published. Required fields are marked *

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Anti-spam image