3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
10 namespace Zend\Captcha
;
12 use DirectoryIterator
;
13 use Zend\Stdlib\ErrorHandler
;
16 * Image-based captcha element
18 * Generates image displaying random word
20 class Image
extends AbstractWord
23 * Directory for generated images
27 protected $imgDir = "public/images/captcha/";
30 * URL for accessing images
34 protected $imgUrl = "/images/captcha/";
37 * Image's alt tag content
41 protected $imgAlt = "";
44 * Image suffix (including dot)
48 protected $suffix = ".png";
55 protected $width = 200;
62 protected $height = 50;
69 protected $fsize = 24;
79 * Image to use as starting point
80 * Default is blank image. If provided, should be PNG image.
84 protected $startImage;
87 * How frequently to execute garbage collection
91 protected $gcFreq = 10;
94 * How long to keep generated images
98 protected $expiration = 600;
101 * Number of noise dots on image
102 * Used twice - before and after transform
106 protected $dotNoiseLevel = 100;
109 * Number of noise lines on image
110 * Used twice - before and after transform
114 protected $lineNoiseLevel = 5;
119 * @param array|\Traversable $options
120 * @throws Exception\ExtensionNotLoadedException
122 public function __construct($options = null)
124 if (!extension_loaded("gd")) {
125 throw new Exception\
ExtensionNotLoadedException("Image CAPTCHA requires GD extension");
128 if (!function_exists("imagepng")) {
129 throw new Exception\
ExtensionNotLoadedException("Image CAPTCHA requires PNG support");
132 if (!function_exists("imageftbbox")) {
133 throw new Exception\
ExtensionNotLoadedException("Image CAPTCHA requires FT fonts support");
136 parent
::__construct($options);
142 public function getImgAlt()
144 return $this->imgAlt
;
150 public function getStartImage()
152 return $this->startImage
;
158 public function getDotNoiseLevel()
160 return $this->dotNoiseLevel
;
166 public function getLineNoiseLevel()
168 return $this->lineNoiseLevel
;
172 * Get captcha expiration
176 public function getExpiration()
178 return $this->expiration
;
182 * Get garbage collection frequency
186 public function getGcFreq()
188 return $this->gcFreq
;
192 * Get font to use when generating captcha
196 public function getFont()
206 public function getFontSize()
212 * Get captcha image height
216 public function getHeight()
218 return $this->height
;
222 * Get captcha image directory
226 public function getImgDir()
228 return $this->imgDir
;
232 * Get captcha image base URL
236 public function getImgUrl()
238 return $this->imgUrl
;
242 * Get captcha image file suffix
246 public function getSuffix()
248 return $this->suffix
;
252 * Get captcha image width
256 public function getWidth()
262 * @param string $startImage
265 public function setStartImage($startImage)
267 $this->startImage
= $startImage;
272 * @param int $dotNoiseLevel
275 public function setDotNoiseLevel($dotNoiseLevel)
277 $this->dotNoiseLevel
= $dotNoiseLevel;
282 * @param int $lineNoiseLevel
285 public function setLineNoiseLevel($lineNoiseLevel)
287 $this->lineNoiseLevel
= $lineNoiseLevel;
292 * Set captcha expiration
294 * @param int $expiration
297 public function setExpiration($expiration)
299 $this->expiration
= $expiration;
304 * Set garbage collection frequency
309 public function setGcFreq($gcFreq)
311 $this->gcFreq
= $gcFreq;
318 * @param string $font
321 public function setFont($font)
328 * Set captcha font size
333 public function setFontSize($fsize)
335 $this->fsize
= $fsize;
340 * Set captcha image height
345 public function setHeight($height)
347 $this->height
= $height;
352 * Set captcha image storage directory
354 * @param string $imgDir
357 public function setImgDir($imgDir)
359 $this->imgDir
= rtrim($imgDir, "/\\") . '/';
364 * Set captcha image base URL
366 * @param string $imgUrl
369 public function setImgUrl($imgUrl)
371 $this->imgUrl
= rtrim($imgUrl, "/\\") . '/';
376 * @param string $imgAlt
379 public function setImgAlt($imgAlt)
381 $this->imgAlt
= $imgAlt;
386 * Set captcha image filename suffix
388 * @param string $suffix
391 public function setSuffix($suffix)
393 $this->suffix
= $suffix;
398 * Set captcha image width
403 public function setWidth($width)
405 $this->width
= $width;
410 * Generate random frequency
414 protected function randomFreq()
416 return mt_rand(700000, 1000000) / 15000000;
420 * Generate random phase
424 protected function randomPhase()
426 // random phase from 0 to pi
427 return mt_rand(0, 3141592) / 1000000;
431 * Generate random character size
435 protected function randomSize()
437 return mt_rand(300, 700) / 100;
443 * @return string captcha ID
445 public function generate()
447 $id = parent
::generate();
450 // If there's already such file, try creating a new ID
451 while ($tries-- && file_exists($this->getImgDir() . $id . $this->getSuffix())) {
452 $id = $this->generateRandomId();
455 $this->generateImage($id, $this->getWord());
457 if (mt_rand(1, $this->getGcFreq()) == 1) {
465 * Generate image captcha
467 * Override this function if you want different image generator
468 * Wave transform from http://www.captcha.ru/captchas/multiwave/
470 * @param string $id Captcha ID
471 * @param string $word Captcha word
472 * @throws Exception\NoFontProvidedException if no font was set
473 * @throws Exception\ImageNotLoadableException if start image cannot be loaded
475 protected function generateImage($id, $word)
477 $font = $this->getFont();
480 throw new Exception\
NoFontProvidedException('Image CAPTCHA requires font');
483 $w = $this->getWidth();
484 $h = $this->getHeight();
485 $fsize = $this->getFontSize();
487 $imgFile = $this->getImgDir() . $id . $this->getSuffix();
489 if (empty($this->startImage
)) {
490 $img = imagecreatetruecolor($w, $h);
492 // Potential error is change to exception
493 ErrorHandler
::start();
494 $img = imagecreatefrompng($this->startImage
);
495 $error = ErrorHandler
::stop();
496 if (!$img ||
$error) {
497 throw new Exception\
ImageNotLoadableException(
498 "Can not load start image '{$this->startImage}'",
507 $textColor = imagecolorallocate($img, 0, 0, 0);
508 $bgColor = imagecolorallocate($img, 255, 255, 255);
509 imagefilledrectangle($img, 0, 0, $w-1, $h-1, $bgColor);
510 $textbox = imageftbbox($fsize, 0, $font, $word);
511 $x = ($w - ($textbox[2] - $textbox[0])) / 2;
512 $y = ($h - ($textbox[7] - $textbox[1])) / 2;
513 imagefttext($img, $fsize, 0, $x, $y, $textColor, $font, $word);
516 for ($i=0; $i < $this->dotNoiseLevel
; $i++
) {
517 imagefilledellipse($img, mt_rand(0, $w), mt_rand(0, $h), 2, 2, $textColor);
519 for ($i=0; $i < $this->lineNoiseLevel
; $i++
) {
520 imageline($img, mt_rand(0, $w), mt_rand(0, $h), mt_rand(0, $w), mt_rand(0, $h), $textColor);
524 $img2 = imagecreatetruecolor($w, $h);
525 $bgColor = imagecolorallocate($img2, 255, 255, 255);
526 imagefilledrectangle($img2, 0, 0, $w-1, $h-1, $bgColor);
528 // apply wave transforms
529 $freq1 = $this->randomFreq();
530 $freq2 = $this->randomFreq();
531 $freq3 = $this->randomFreq();
532 $freq4 = $this->randomFreq();
534 $ph1 = $this->randomPhase();
535 $ph2 = $this->randomPhase();
536 $ph3 = $this->randomPhase();
537 $ph4 = $this->randomPhase();
539 $szx = $this->randomSize();
540 $szy = $this->randomSize();
542 for ($x = 0; $x < $w; $x++
) {
543 for ($y = 0; $y < $h; $y++
) {
544 $sx = $x +
(sin($x*$freq1 +
$ph1) +
sin($y*$freq3 +
$ph3)) * $szx;
545 $sy = $y +
(sin($x*$freq2 +
$ph2) +
sin($y*$freq4 +
$ph4)) * $szy;
547 if ($sx < 0 ||
$sy < 0 ||
$sx >= $w - 1 ||
$sy >= $h - 1) {
550 $color = (imagecolorat($img, $sx, $sy) >> 16) & 0xFF;
551 $colorX = (imagecolorat($img, $sx +
1, $sy) >> 16) & 0xFF;
552 $colorY = (imagecolorat($img, $sx, $sy +
1) >> 16) & 0xFF;
553 $colorXY = (imagecolorat($img, $sx +
1, $sy +
1) >> 16) & 0xFF;
556 if ($color == 255 && $colorX == 255 && $colorY == 255 && $colorXY == 255) {
559 } elseif ($color == 0 && $colorX == 0 && $colorY == 0 && $colorXY == 0) {
560 // transfer inside of the image as-is
563 // do antialiasing for border items
564 $fracX = $sx - floor($sx);
565 $fracY = $sy - floor($sy);
566 $fracX1 = 1 - $fracX;
567 $fracY1 = 1 - $fracY;
569 $newcolor = $color * $fracX1 * $fracY1
570 +
$colorX * $fracX * $fracY1
571 +
$colorY * $fracX1 * $fracY
572 +
$colorXY * $fracX * $fracY;
575 imagesetpixel($img2, $x, $y, imagecolorallocate($img2, $newcolor, $newcolor, $newcolor));
580 for ($i=0; $i<$this->dotNoiseLevel
; $i++
) {
581 imagefilledellipse($img2, mt_rand(0, $w), mt_rand(0, $h), 2, 2, $textColor);
584 for ($i=0; $i<$this->lineNoiseLevel
; $i++
) {
585 imageline($img2, mt_rand(0, $w), mt_rand(0, $h), mt_rand(0, $w), mt_rand(0, $h), $textColor);
588 imagepng($img2, $imgFile);
594 * Remove old files from image directory
597 protected function gc()
599 $expire = time() - $this->getExpiration();
600 $imgdir = $this->getImgDir();
601 if (!$imgdir ||
strlen($imgdir) < 2) {
606 $suffixLength = strlen($this->suffix
);
607 foreach (new DirectoryIterator($imgdir) as $file) {
608 if (!$file->isDot() && !$file->isDir()) {
609 if (file_exists($file->getPathname()) && $file->getMTime() < $expire) {
610 // only deletes files ending with $this->suffix
611 if (substr($file->getFilename(), -($suffixLength)) == $this->suffix
) {
612 unlink($file->getPathname());
620 * Get helper name used to render captcha
624 public function getHelperName()
626 return 'captcha/image';