capictcha
Project Page

MAIN DOCS DOWNLOAD
<?php

/*
* CAPicTCHA.php v0.3
* (c) 2004 Wil Langford
* Licensed under the Academic Free License version 2.1

* A CAPTCHA generator function.  Takes four arguments via GET or POST:

* text = the text you want to output as a CAPTCHA graphic
* height = the approximate height in pixels of the output graphic (default=50)
* output = png, jpeg or text (for debugging) (default=png)
* special = <hex md5 hash> (default=<unset>)

* If special is set and appears to be an MD5 hash, the program attempts to lookup
* the related value in a separate file.

**************************************************************
* Credits (examples, documentation, etc. that really helped) *
**************************************************************

***** Turing String Image Generator (the original foundation code for this program)
* http://viebrock.ca/downloads/turing-image.phps
* Copyright (c) 2004 Colin Viebrock

***** Jeff Knight from NYPHP's "Introduction to PHP Image Functions" (excellent guide)
* http://www.nyphp.org/content/presentations/GDintro/

***** MK12 (sweet design company - graphics, animation, fonts, etc.  check them out)
* http://www.mk12.com
* The studscratch font is theirs, and is used and redistributed with permission. 
* This permission is conditional and remains in force only as long as the
* project remains Open Source.

***** Ray Larabie (professional font designer)
* http://www.larabiefonts.com
* Crystal Radio Kit is his, and it's the reliable, workhorse font of capictcha.
* I like studscratch, but the Crystal Radio Kit font is much more complete, and
* is also a lot cleaner looking for cases where a studscratch character renders
* particularly unclearly (e.g. lowercase "a").

***** See ABOUT-FONTS for additional font credits.

***** FontForge (for font conversions while I was hunting for the right font)
* http://fontforge.sourceforge.net/

* And, of course, PHP and other fine open source projects.
*/


/**************************************************
           START CONFIGURATION SECTION 
              aka YE OLDE CONSTANTS
 **************************************************/

// where to find necessary files
    
define('BASEDIR',dirname($_SERVER['SCRIPT_FILENAME']) . "/");
    
define('FONTBASEDIR',BASEDIR);
// where is the lookup hashfile stored, if we are using one.  must be readable by web server, but should
// NOT BE SERVED BY THE WEB SERVER.  it would kind of defeat the purpose to do so, because anyone would be
// able to grab the list of things you are trying to obscure from machines
    
define('HASHFILE',BASEDIR "../../.capictcha.hash");
// what font file to use - MUST be an absolute path unless the font in a GD library defined font path
    
define('FONT',FONTBASEDIR "MK12.studscratch.ttf");
// what chars can we use from FONT and still have them be readable?
    
define('FONTCHARS'," ._+!$0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
// if a character is not in FONTCHARS, which font should we use?  If FONT is a standard font with all the
// punctuation, numbers, etc. in it, then you should be able to set STABLEFONT = FONT.  If the FONT you use
// doesn't handle all characters you need, use a more standard font here to catch all the oddball chars.
    
define('STABLEFONT',FONTBASEDIR "LARABIE.crystal-radio-kit.ttf");

// input defaults
    
define('DEFAULT_HEIGHT',100);
    
define('DEFAULT_TYPE',"png");
    
define('DEFAULT_TEXT',"NOT SO RANDOM");

// **********************
// ye olde various limits
// **********************

// what's the longest text string we want to accept?
    
define('MAX_TEXTLEN',100);

// limits on the **approximate** height of the overall graphic displayed
// the actual graphic size is calculated after characters are placed
    
define('MIN_GRAPHICHEIGHT',20);
    
define('MAX_GRAPHICHEIGHT',400);

// set range for font size relative to graphic_height minus padding
// actual max font size is approximately = $graphic_height * 1/Y_PADDING_FACTOR * MAX_FSIZE
    
define('MAX_FSIZE'1.0);
// actual min font size is approximately = $graphic_height * 1/Y_PADDING_FACTOR * MAX_FSIZE * MIN_FSIZE
    
define('MIN_FSIZE'0.7);

// background lower and upper limits for each color component (i.e. R, G and B) out of 255
    
define('MIN_BGCOMPCOL'60);
    
define('MAX_BGCOMPCOL'160);

// font lower and upper limits for each color component (i.e. R, G and B) out of 255
    
define('MIN_FCOMPCOL'100);
    
define('MAX_FCOMPCOL'200);

// minimum and maximum rotation limits for each character
    
define('MIN_ROT', -20);
    
define('MAX_ROT', -MIN_ROT);

// number of overstrikes.  zero disables. one is about the max you really want to use, but hey, go nuts if you want.
    
define('OVERSTRIKES',1);
// if this is greater than zero, then each character is printed twice.  the
// second time it is printed, it is offset +- MAX_OVERSTRIKE_OFFSET both
// horizontally and vertically this quickly makes the text very hard to read,
// so low values are preferred.
    
define('MAX_OVERSTRIKE_OFFSET'3);  // +- pixels
// same as above, except rotates the overstruck character
    
define('MAX_OVERSTRIKE_ROTATE'15); // +- degrees
// same as above, except resizes the overstruck character
    
define('MAX_OVERSTRIKE_RESIZE'22); // +- percentage of size

// maximum number of random lines to draw
// also limited by the number of characters
    
define('MAX_LINES',10);
    
define('MAX_LINE_THICKNESS'6);

// pad the whole graphic output
    
define('X_PADDING_FACTOR'1.05);
    
define('Y_PADDING_FACTOR'1.4);

// range [0-100]
    
define('JPEG_QUALITY'90);

// A setting of 1.0 uses the standard ranges of color for foreground and background.
// higher values mean higher contrast and more readable text
// lower values mean lower contrast and more secure text
// note that the difference between 1.0 and 1.2 is very noticeable
    
define('CONTRAST_MULTIPLIER'1.24);

/**************************************************
            END CONFIGURATION SECTION
 **************************************************/

    
define('TRYDEBUG'FALSE);
    
define('LOG'TRUE);
    
define('LOGFILE',"/tmp/wil-www/capictcha/debug-log");

// ye olde debugging stuff
    
define('LIMITDEBUGBYIP'TRUE);
    
define('DEBUG_IPS'"127.0.0.1");

// decide whether to enter DEBUG mode
if(TRYDEBUG and (!LIMITDEBUGBYIP or (preg_match("/" preg_quote(getenv("REMOTE_ADDR"), "/") . "/"DEBUG_IPS)))) {
    
define('DEBUG',TRUE);
} else {
    
define('DEBUG',FALSE);
}

// if we're debugging, set some stuff up
if(DEBUG) {
    
error_reporting(E_ALL);
    
// error_reporting(E_USER_NOTICE);
} else {
    
error_reporting(E_NONE);
}

// if we're logging, set some stuff up
if(LOG) {
    
ini_set('log_errors',TRUE);
    
ini_set('error_log',LOGFILE);
//    mylog("Remote IP", getenv("REMOTE_ADDR"));
} else {
    if(
file_exists(LOGFILE)) { unlinkLOGFILE ); }
}

// are we using character specific tweaks?  if so, set up the tweaks data structure
// y_pos is a fraction of the char_height to add (positive values mean lower char position)
// char_width is a multiplier for the char_width
define('TWEAKS',TRUE);
if (
TWEAKS) {
    
$tweaks = array(  // whole tweaks array (by character)
        
'MK12.studscratch.ttf' => array(
            
'i' => array( 'char_width'=>0.54),
            
'-' => array( 'y_pos'=>0.5),
        ),
    );    
}

// scan for inputs
foreach (array("text","height","output","special") as $varname) { ${"in_" $varname}=global_capsule($varname); }

// if we're not debugging, then disallow text output
if(!DEBUG) {
    
$in_output = ($in_output === "text") ? "png" $in_output;
} else {
// if we are debugging, set up some stuff
    
mylog("DEBUGGING ACTIVE");
// for convenience, this is a hash that gets created, used once and then destroyed.  it could have
// been more efficient as a switch/case construct, but this looks prettier.
//    ...  plus, special_hash sounds like something nasty your parents made you eat as a child
}

// set up specials
$special_hash = array(
    
'punk' => "'" '"/,.!@#$%^&*()_-=\|+ ',
    
'hello' => 'Hi there!',
    
'person' => 'SSGTAC',
    
'random' => '12345789abdefghijmnqrtuyABCDEFGHIJKLMNOPQRSTUVWXYZ',
);
if(isset(
$special_hash[$in_special])) { $in_text $special_hash[$in_special]; }

// if the special value is random with a number after it, make a random string the length of the number
if(strlen($in_special)>and substr($in_special,0,6) === "random") {
    
$len_rand substr($in_special,6);
    
mylog ("LEN_RAND",$len_rand);
    
$in_text='';
    for(
$i=0$i<$len_rand$i++) {
        
$in_text .= $special_hash['random']{mt_rand(0,strlen($special_hash['random'])-1)};
    }
    
// $in_text = "random not happy"; 
}

// if the special value looks like a hex MD5 hash, try looking it up
if (strlen(preg_replace("/[^0-9a-fA-F]/","",$in_special)) === 32) {
    
mylog("md5",$in_special);
    
$lookupcmd="grep -i " $in_special " " HASHFILE " | cut -f 1 -d ':'";
    
$in_text=exec($lookupcmd);
    if (
strlen($in_text)<2) {
        
$in_text="NOT FOUND";
    }
}

captcha_anything($in_text,$in_height,$in_output);

function 
captcha_anything($text DEFAULT_TEXT,$graphic_height DEFAULT_HEIGHT,$output DEFAULT_TYPE)
{

// use font specific tweaks?
if(TWEAKS) {global $tweaks;}
    
// argument reality checks
// truncate text string to MAX_TEXTLEN chars if longer - provide default value for text
if (strlen($text) > MAX_TEXTLEN) { $text substr($text,0,MAX_TEXTLEN); }
if (
strlen($text) == 0) { $text "NOT SO RANDOM"; }
// graphic height can range from MIN_GRAPHICHEIGHT to MAX_GRAPHICHEIGHT
if ((int)$graphic_height MIN_GRAPHICHEIGHT or (int)$graphic_height MAX_GRAPHICHEIGHT) { $graphic_height DEFAULT_HEIGHT; }
// output types are png, jpeg, and text
if ($output !== "png" and $output !== "jpeg" and $output !== "text") { $output "png"; }

// init vars
    
$data = array();
    
$graphic_width 0;

// set y padding
// NOTE: Y padding is mostly allocated on the bottom to allow characters with tails some room (e.g. "gpqyj")
define('Y_PADDING',$graphic_height*(Y_PADDING_FACTOR-1));
// this is a fraction of the Y_PADDING to allocate on the bottom
define('Y_PADDING_BOTTOM',0.7);
    
// build the data array of the characters, size, placement, etc.
    
for($i=0$i<strlen($text); $i++) {
        
$char substr($text$i1);
        
$size mt_rand(($graphic_height Y_PADDING) * MIN_FSIZE MAX_FSIZE, ($graphic_height Y_PADDING) * MAX_FSIZE);
        
$angle mt_rand(MIN_ROTMAX_ROT);
        
$font = (preg_match("/" preg_quote("$char","/") . "/"FONTCHARS )) ? FONT STABLEFONT;
            
$fontbase basename($font);
        
$bbox imagettfbbox( (float)$size, (float)$angle$font$char );
            
$char_width max($bbox[2],$bbox[4]) - min($bbox[0],$bbox[6]);
            
$char_height max($bbox[1],$bbox[3]) - min($bbox[7],$bbox[5]);
        
$pos_x = ($i>0) ? $data[$i-1]['pos_x'] + $data[$i-1]['char_width']:0;
        
$pos_y $graphic_height Y_PADDING*Y_PADDING_BOTTOM ;
        if(
TWEAKS) {
            if(isset(
$tweaks[$fontbase][$char])){
                foreach(
$tweaks[$fontbase][$char] as $attribute => $adjust) {
                    switch (
$attribute) {
                        case 
'char_width':
                            $
$attribute*=$adjust;
                        break;
                        case 
'char_height':
                            $
$attribute*=$adjust;
                        break;
                        case 
'pos_y':
                            $
$attribute+=$char_height*$adjust;
                        break;
                    }
                }
            }
        }
        
$graphic_width += $char_width;
        
$data[$i] = array(
            
'char'    => $char,
            
'size'    => $size,
            
'angle'    => $angle,
            
'font'    => $font,
            
'fontbase'    => $fontbase,
            
'char_height'    => $char_height,
            
'char_width'    => $char_width,
            
'pos_x'    => $pos_x,
            
'pos_y'    => $pos_y,
            
'color'    => 0,            //  we can't allocate colors yet, because the image doesn't exist yet - 0 is placeholder value
        
);
        if(
OVERSTRIKES>0) {
            for(
$j=0$j<OVERSTRIKES$j++) {
                
$data[$i]['OS'][$j] = array (
                    
'size'    => $size*(1+(mt_rand(-MAX_OVERSTRIKE_RESIZE,MAX_OVERSTRIKE_RESIZE)/100)),
                    
'angle'    => $angle+mt_rand(-MAX_OVERSTRIKE_ROTATE,MAX_OVERSTRIKE_ROTATE),
                    
'pos_x'    => $pos_x+mt_rand(-MAX_OVERSTRIKE_OFFSET,MAX_OVERSTRIKE_OFFSET),
                    
'pos_y'    => $pos_y+mt_rand(-MAX_OVERSTRIKE_OFFSET,MAX_OVERSTRIKE_OFFSET),
                );
            }
        }
    }

// calculate the final image size, adding some padding
    
define('X_PADDING',$graphic_width*(X_PADDING_FACTOR-1));
    
$graphic_width += X_PADDING;

// build image
    
$im imagecreate($graphic_width$graphic_height);

// allocate the colors
    
$color_bg=imagecolorallocate($imcontrast_color("bg"),contrast_color("bg"),contrast_color("bg"));
    if(
DEBUG) {
        
$color_white=imagecolorallocate($im255,255,255);
        
$color_black=imagecolorallocate($im0,0,0);
    }
    
$color_border=imagecolorallocate($im0,0,0);
    for(
$i=0$i<strlen($text); $i++) {
        
$data[$i]['color'] = imagecolorallocate($imcontrast_color("fg"), contrast_color("fg"), contrast_color("fg"));
        
$line_color[$i]=imagecolorallocate($imcontrast_color("bg"), contrast_color("bg"), contrast_color("bg"));
    }

// make the random background lines
    
for($l=0$l<min(MAX_LINES,strlen($text)-1); $l++) {
        
$thick mt_rand(1,MAX_LINE_THICKNESS);
        if(
$l == 1) {  // alternate between top-to-bottom and side-to-side lines
            
$line_data[$l] = array( // horizontal
                
0,mt_rand(0,$graphic_height),
                
$graphic_width,mt_rand(0,$graphic_height),
                
$graphic_width,0,
                
0,0,
            );
            
$line_data[$l][5] = $line_data[$l][3] + $thick;
            
$line_data[$l][7] = $line_data[$l][1] + $thick;
        } else {
            
$line_data[$l] = array( // vertical
                
mt_rand(0,$graphic_width),0,
                
mt_rand(0,$graphic_width),$graphic_height,
                
0,$graphic_height,
                
0,0,
            );
            
$line_data[$l][4] = $line_data[$l][2] + $thick;
            
$line_data[$l][6] = $line_data[$l][0] + $thick;
        }
        
imagefilledpolygon($im$line_data[$l], 4$line_color[$l]);
    }

// output each character
    
$l=0;
    foreach(
$data as $d) {
        
//  calculate absolute character position
        
$d['pos_x'] += X_PADDING/2;
        
// actually place each char into image
        
if(DEBUG) { imagettftext($im$graphic_height*.10$d['pos_x'], $graphic_height*.9$color_blackSTABLEFONT$d['char'] ); }
        
imagettftext($im$d['size'], $d['angle'], $d['pos_x'], $d['pos_y'], $d['color'], $d['font'], $d['char'] );
        
//  imagefilledpolygon($im, $line_data[$l], 4, $line_color[$l++]);  // interleave images and lines - looks bad - disabline for now
        
if(OVERSTRIKES>0) {
            for(
$j=0$j<OVERSTRIKES$j++) {
                
//  calculate absolute character position
                
$d['OS'][$j]['pos_x'] +=  X_PADDING/2;
                
// actually place each char into image
                
imagettftext($im$d['OS'][$j]['size'], $d['OS'][$j]['angle'], $d['OS'][$j]['pos_x'], $d['OS'][$j]['pos_y'], $d['color'], $d['font'], $d['char'] );
            }
        }
    }

// a nice border
    
imagerectangle($im00$graphic_width-1$graphic_height-1$color_border);

// display it
    
switch ($output) {
        case 
'text':
            if(
DEBUG) {
                echo 
"<pre>\n";
                echo 
"DEBUGGING OUTPUT\n";
                
print_r($data);
                
print_r($line_data);
                echo 
"\n</pre>\n";
            }
            break;
        case 
'jpeg':
            
header('Content-type: image/jpeg');
            
imagejpeg($im,"",JPEG_QUALITY);
            break;
        case 
'png':
        default:
            
header('Content-type: image/png');
            
imagepng($im);
            break;
    }
    
imagedestroy($im);
}


function 
contrast_color($style="none")
{
    
$mincol=0;
    
$maxcol=255;
    switch (
$style) {
        case 
"bg":
            
$mincol=round(MIN_BGCOMPCOL/CONTRAST_MULTIPLIER);
            
$maxcol=round(MAX_BGCOMPCOL/CONTRAST_MULTIPLIER);
        break;
        case 
"fg":
            
$mincol=round(MIN_FCOMPCOL*CONTRAST_MULTIPLIER);
            
$maxcol=round(MAX_FCOMPCOL*CONTRAST_MULTIPLIER);
        break;
        case 
"bgno":
            
$mincol=MIN_BGCOMPCOL;
            
$maxcol=MAX_BGCOMPCOL;
        break;
        case 
"fgno":
            
$mincol=MIN_FCOMPCOL;
            
$maxcol=MAX_FCOMPCOL;
        break;
    }
    
$mincol=max(0,min(255,$mincol));
    
$maxcol=max(0,min(255,$maxcol));
    
$out=gen_color($mincol,$maxcol);
    return 
$out;
}

function 
gen_color($mincol,$maxcol)
{
    
$out=mt_rand($mincol,$maxcol);
    return 
$out;
}

function 
global_capsule($globalname)
{
    
$filtered_global "";

    if(isset(
$_POST[$globalname])) {
        
$filtered_global strip_tags(urldecode($_POST[$globalname]));
    }

    if(isset(
$_GET[$globalname])) {
        
$filtered_global strip_tags(urldecode($_GET[$globalname]));
    }

    
$filtered_global=substr($filtered_global,0,255);

    return 
$filtered_global;
}

function 
mylog()
{
    
$logthis=func_get_args();
    if(
DEBUG) {trigger_error( (is_array($logthis) ? implode(":"$logthis) : $logthis) , E_USER_NOTICE);}
}
?>