vendor/gregwar/captcha/src/Gregwar/Captcha/CaptchaBuilder.php line 407

Open in your IDE?
  1. <?php
  2. namespace Gregwar\Captcha;
  3. use \Exception;
  4. /**
  5.  * Builds a new captcha image
  6.  * Uses the fingerprint parameter, if one is passed, to generate the same image
  7.  *
  8.  * @author Gregwar <g.passault@gmail.com>
  9.  * @author Jeremy Livingston <jeremy.j.livingston@gmail.com>
  10.  */
  11. class CaptchaBuilder implements CaptchaBuilderInterface
  12. {
  13.     /**
  14.      * @var array
  15.      */
  16.     protected $fingerprint = array();
  17.     /**
  18.      * @var bool
  19.      */
  20.     protected $useFingerprint false;
  21.     /**
  22.      * @var array
  23.      */
  24.     protected $textColor = array();
  25.     /**
  26.      * @var array
  27.      */
  28.     protected $lineColor null;
  29.     /**
  30.      * @var array
  31.      */
  32.     protected $backgroundColor null;
  33.     /**
  34.      * @var array
  35.      */
  36.     protected $backgroundImages = array();
  37.     /**
  38.      * @var resource
  39.      */
  40.     protected $contents null;
  41.     /**
  42.      * @var string
  43.      */
  44.     protected $phrase null;
  45.     /**
  46.      * @var PhraseBuilderInterface
  47.      */
  48.     protected $builder;
  49.     /**
  50.      * @var bool
  51.      */
  52.     protected $distortion true;
  53.     /**
  54.      * The maximum number of lines to draw in front of
  55.      * the image. null - use default algorithm
  56.      */
  57.     protected $maxFrontLines null;
  58.     /**
  59.      * The maximum number of lines to draw behind
  60.      * the image. null - use default algorithm
  61.      */
  62.     protected $maxBehindLines null;
  63.     /**
  64.      * The maximum angle of char
  65.      */
  66.     protected $maxAngle 8;
  67.     /**
  68.      * The maximum offset of char
  69.      */
  70.     protected $maxOffset 5;
  71.     /**
  72.      * Is the interpolation enabled ?
  73.      *
  74.      * @var bool
  75.      */
  76.     protected $interpolation true;
  77.     /**
  78.      * Ignore all effects
  79.      *
  80.      * @var bool
  81.      */
  82.     protected $ignoreAllEffects false;
  83.     /**
  84.      * Allowed image types for the background images
  85.      *
  86.      * @var array
  87.      */
  88.     protected $allowedBackgroundImageTypes = array('image/png''image/jpeg''image/gif');
  89.     /**
  90.      * The image contents
  91.      */
  92.     public function getContents()
  93.     {
  94.         return $this->contents;
  95.     }
  96.     /**
  97.      * Enable/Disables the interpolation
  98.      *
  99.      * @param $interpolate bool  True to enable, false to disable
  100.      *
  101.      * @return CaptchaBuilder
  102.      */
  103.     public function setInterpolation($interpolate true)
  104.     {
  105.         $this->interpolation $interpolate;
  106.         return $this;
  107.     }
  108.     /**
  109.      * Temporary dir, for OCR check
  110.      */
  111.     public $tempDir 'temp/';
  112.     public function __construct($phrase nullPhraseBuilderInterface $builder null)
  113.     {
  114.         if ($builder === null) {
  115.             $this->builder = new PhraseBuilder;
  116.         } else {
  117.             $this->builder $builder;
  118.         }
  119.         $this->phrase is_string($phrase) ? $phrase $this->builder->build($phrase);
  120.     }
  121.     /**
  122.      * Setting the phrase
  123.      */
  124.     public function setPhrase($phrase)
  125.     {
  126.         $this->phrase = (string) $phrase;
  127.     }
  128.     /**
  129.      * Enables/disable distortion
  130.      */
  131.     public function setDistortion($distortion)
  132.     {
  133.         $this->distortion = (bool) $distortion;
  134.         return $this;
  135.     }
  136.     public function setMaxBehindLines($maxBehindLines)
  137.     {
  138.         $this->maxBehindLines $maxBehindLines;
  139.         return $this;
  140.     }
  141.     public function setMaxFrontLines($maxFrontLines)
  142.     {
  143.         $this->maxFrontLines $maxFrontLines;
  144.         return $this;
  145.     }
  146.     public function setMaxAngle($maxAngle)
  147.     {
  148.         $this->maxAngle $maxAngle;
  149.         return $this;
  150.     }
  151.     public function setMaxOffset($maxOffset)
  152.     {
  153.         $this->maxOffset $maxOffset;
  154.         return $this;
  155.     }
  156.     /**
  157.      * Gets the captcha phrase
  158.      */
  159.     public function getPhrase()
  160.     {
  161.         return $this->phrase;
  162.     }
  163.     /**
  164.      * Returns true if the given phrase is good
  165.      */
  166.     public function testPhrase($phrase)
  167.     {
  168.         return ($this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase()));
  169.     }
  170.     /**
  171.      * Instantiation
  172.      */
  173.     public static function create($phrase null)
  174.     {
  175.         return new self($phrase);
  176.     }
  177.     /**
  178.      * Sets the text color to use
  179.      */
  180.     public function setTextColor($r$g$b)
  181.     {
  182.         $this->textColor = array($r$g$b);
  183.         return $this;
  184.     }
  185.     /**
  186.      * Sets the background color to use
  187.      */
  188.     public function setBackgroundColor($r$g$b)
  189.     {
  190.         $this->backgroundColor = array($r$g$b);
  191.         return $this;
  192.     }
  193.     public function setLineColor($r$g$b)
  194.     {
  195.         $this->lineColor = array($r$g$b);
  196.         return $this;
  197.     }
  198.     /**
  199.      * Sets the ignoreAllEffects value
  200.      *
  201.      * @param bool $ignoreAllEffects
  202.      * @return CaptchaBuilder
  203.      */
  204.     public function setIgnoreAllEffects($ignoreAllEffects)
  205.     {
  206.         $this->ignoreAllEffects $ignoreAllEffects;
  207.         return $this;
  208.     }
  209.     /**
  210.      * Sets the list of background images to use (one image is randomly selected)
  211.      */
  212.     public function setBackgroundImages(array $backgroundImages)
  213.     {
  214.         $this->backgroundImages $backgroundImages;
  215.         return $this;
  216.     }
  217.     /**
  218.      * Draw lines over the image
  219.      */
  220.     protected function drawLine($image$width$height$tcol null)
  221.     {
  222.         if ($this->lineColor === null) {
  223.             $red $this->rand(100255);
  224.             $green $this->rand(100255);
  225.             $blue $this->rand(100255);
  226.         } else {
  227.             $red $this->lineColor[0];
  228.             $green $this->lineColor[1];
  229.             $blue $this->lineColor[2];
  230.         }
  231.         if ($tcol === null) {
  232.             $tcol imagecolorallocate($image$red$green$blue);
  233.         }
  234.         if ($this->rand(01)) { // Horizontal
  235.             $Xa   $this->rand(0$width/2);
  236.             $Ya   $this->rand(0$height);
  237.             $Xb   $this->rand($width/2$width);
  238.             $Yb   $this->rand(0$height);
  239.         } else { // Vertical
  240.             $Xa   $this->rand(0$width);
  241.             $Ya   $this->rand(0$height/2);
  242.             $Xb   $this->rand(0$width);
  243.             $Yb   $this->rand($height/2$height);
  244.         }
  245.         imagesetthickness($image$this->rand(13));
  246.         imageline($image$Xa$Ya$Xb$Yb$tcol);
  247.     }
  248.     /**
  249.      * Apply some post effects
  250.      */
  251.     protected function postEffect($image)
  252.     {
  253.         if (!function_exists('imagefilter')) {
  254.             return;
  255.         }
  256.         if ($this->backgroundColor != null || $this->textColor != null) {
  257.             return;
  258.         }
  259.         // Negate ?
  260.         if ($this->rand(01) == 0) {
  261.             imagefilter($imageIMG_FILTER_NEGATE);
  262.         }
  263.         // Edge ?
  264.         if ($this->rand(010) == 0) {
  265.             imagefilter($imageIMG_FILTER_EDGEDETECT);
  266.         }
  267.         // Contrast
  268.         imagefilter($imageIMG_FILTER_CONTRAST$this->rand(-5010));
  269.         // Colorize
  270.         if ($this->rand(05) == 0) {
  271.             imagefilter($imageIMG_FILTER_COLORIZE$this->rand(-8050), $this->rand(-8050), $this->rand(-8050));
  272.         }
  273.     }
  274.     /**
  275.      * Writes the phrase on the image
  276.      */
  277.     protected function writePhrase($image$phrase$font$width$height)
  278.     {
  279.         $length mb_strlen($phrase);
  280.         if ($length === 0) {
  281.             return \imagecolorallocate($image000);
  282.         }
  283.         // Gets the text size and start position
  284.         $size $width $length $this->rand(03) - 1;
  285.         $box \imagettfbbox($size0$font$phrase);
  286.         $textWidth $box[2] - $box[0];
  287.         $textHeight $box[1] - $box[7];
  288.         $x = ($width $textWidth) / 2;
  289.         $y = ($height $textHeight) / $size;
  290.         if (!$this->textColor) {
  291.             $textColor = array($this->rand(0150), $this->rand(0150), $this->rand(0150));
  292.         } else {
  293.             $textColor $this->textColor;
  294.         }
  295.         $col \imagecolorallocate($image$textColor[0], $textColor[1], $textColor[2]);
  296.         // Write the letters one by one, with random angle
  297.         for ($i=0$i<$length$i++) {
  298.             $symbol mb_substr($phrase$i1);
  299.             $box \imagettfbbox($size0$font$symbol);
  300.             $w $box[2] - $box[0];
  301.             $angle $this->rand(-$this->maxAngle$this->maxAngle);
  302.             $offset $this->rand(-$this->maxOffset$this->maxOffset);
  303.             \imagettftext($image$size$angle$x$y $offset$col$font$symbol);
  304.             $x += $w;
  305.         }
  306.         return $col;
  307.     }
  308.     /**
  309.      * Try to read the code against an OCR
  310.      */
  311.     public function isOCRReadable()
  312.     {
  313.         if (!is_dir($this->tempDir)) {
  314.             @mkdir($this->tempDir0755true);
  315.         }
  316.         $tempj $this->tempDir uniqid('captcha'true) . '.jpg';
  317.         $tempp $this->tempDir uniqid('captcha'true) . '.pgm';
  318.         $this->save($tempj);
  319.         shell_exec("convert $tempj $tempp");
  320.         $value trim(strtolower(shell_exec("ocrad $tempp")));
  321.         @unlink($tempj);
  322.         @unlink($tempp);
  323.         return $this->testPhrase($value);
  324.     }
  325.     /**
  326.      * Builds while the code is readable against an OCR
  327.      */
  328.     public function buildAgainstOCR($width 150$height 40$font null$fingerprint null)
  329.     {
  330.         do {
  331.             $this->build($width$height$font$fingerprint);
  332.         } while ($this->isOCRReadable());
  333.     }
  334.     /**
  335.      * Generate the image
  336.      */
  337.     public function build($width 150$height 40$font null$fingerprint null)
  338.     {
  339.         if (null !== $fingerprint) {
  340.             $this->fingerprint $fingerprint;
  341.             $this->useFingerprint true;
  342.         } else {
  343.             $this->fingerprint = array();
  344.             $this->useFingerprint false;
  345.         }
  346.         if ($font === null) {
  347.             $font __DIR__ '/Font/captcha'.$this->rand(05).'.ttf';
  348.         }
  349.         if (empty($this->backgroundImages)) {
  350.             // if background images list is not set, use a color fill as a background
  351.             $image   imagecreatetruecolor($width$height);
  352.             if ($this->backgroundColor == null) {
  353.                 $bg imagecolorallocate($image$this->rand(200255), $this->rand(200255), $this->rand(200255));
  354.             } else {
  355.                 $color $this->backgroundColor;
  356.                 $bg imagecolorallocate($image$color[0], $color[1], $color[2]);
  357.             }
  358.             imagefill($image00$bg);
  359.         } else {
  360.             // use a random background image
  361.             $randomBackgroundImage $this->backgroundImages[rand(0count($this->backgroundImages)-1)];
  362.             $imageType $this->validateBackgroundImage($randomBackgroundImage);
  363.             $image $this->createBackgroundImageFromType($randomBackgroundImage$imageType);
  364.         }
  365.         // Apply effects
  366.         if (!$this->ignoreAllEffects) {
  367.             $square $width $height;
  368.             $effects $this->rand($square/3000$square/2000);
  369.             // set the maximum number of lines to draw in front of the text
  370.             if ($this->maxBehindLines != null && $this->maxBehindLines 0) {
  371.                 $effects min($this->maxBehindLines$effects);
  372.             }
  373.             if ($this->maxBehindLines !== 0) {
  374.                 for ($e 0$e $effects$e++) {
  375.                     $this->drawLine($image$width$height);
  376.                 }
  377.             }
  378.         }
  379.         // Write CAPTCHA text
  380.         $color $this->writePhrase($image$this->phrase$font$width$height);
  381.         // Apply effects
  382.         if (!$this->ignoreAllEffects) {
  383.             $square $width $height;
  384.             $effects $this->rand($square/3000$square/2000);
  385.             // set the maximum number of lines to draw in front of the text
  386.             if ($this->maxFrontLines != null && $this->maxFrontLines 0) {
  387.                 $effects min($this->maxFrontLines$effects);
  388.             }
  389.             if ($this->maxFrontLines !== 0) {
  390.                 for ($e 0$e $effects$e++) {
  391.                     $this->drawLine($image$width$height$color);
  392.                 }
  393.             }
  394.         }
  395.         // Distort the image
  396.         if ($this->distortion && !$this->ignoreAllEffects) {
  397.             $image $this->distort($image$width$height$bg);
  398.         }
  399.         // Post effects
  400.         if (!$this->ignoreAllEffects) {
  401.             $this->postEffect($image);
  402.         }
  403.         $this->contents $image;
  404.         return $this;
  405.     }
  406.     /**
  407.      * Distorts the image
  408.      */
  409.     public function distort($image$width$height$bg)
  410.     {
  411.         $contents imagecreatetruecolor($width$height);
  412.         $X          $this->rand(0$width);
  413.         $Y          $this->rand(0$height);
  414.         $phase      $this->rand(010);
  415.         $scale      1.1 $this->rand(010000) / 30000;
  416.         for ($x 0$x $width$x++) {
  417.             for ($y 0$y $height$y++) {
  418.                 $Vx $x $X;
  419.                 $Vy $y $Y;
  420.                 $Vn sqrt($Vx $Vx $Vy $Vy);
  421.                 if ($Vn != 0) {
  422.                     $Vn2 $Vn sin($Vn 30);
  423.                     $nX  $X + ($Vx $Vn2 $Vn);
  424.                     $nY  $Y + ($Vy $Vn2 $Vn);
  425.                 } else {
  426.                     $nX $X;
  427.                     $nY $Y;
  428.                 }
  429.                 $nY $nY $scale sin($phase $nX 0.2);
  430.                 if ($this->interpolation) {
  431.                     $p $this->interpolate(
  432.                         $nX floor($nX),
  433.                         $nY floor($nY),
  434.                         $this->getCol($imagefloor($nX), floor($nY), $bg),
  435.                         $this->getCol($imageceil($nX), floor($nY), $bg),
  436.                         $this->getCol($imagefloor($nX), ceil($nY), $bg),
  437.                         $this->getCol($imageceil($nX), ceil($nY), $bg)
  438.                     );
  439.                 } else {
  440.                     $p $this->getCol($imageround($nX), round($nY), $bg);
  441.                 }
  442.                 if ($p == 0) {
  443.                     $p $bg;
  444.                 }
  445.                 imagesetpixel($contents$x$y$p);
  446.             }
  447.         }
  448.         return $contents;
  449.     }
  450.     /**
  451.      * Saves the Captcha to a jpeg file
  452.      */
  453.     public function save($filename$quality 90)
  454.     {
  455.         imagejpeg($this->contents$filename$quality);
  456.     }
  457.     /**
  458.      * Gets the image GD
  459.      */
  460.     public function getGd()
  461.     {
  462.         return $this->contents;
  463.     }
  464.     /**
  465.      * Gets the image contents
  466.      */
  467.     public function get($quality 90)
  468.     {
  469.         ob_start();
  470.         $this->output($quality);
  471.         return ob_get_clean();
  472.     }
  473.     /**
  474.      * Gets the HTML inline base64
  475.      */
  476.     public function inline($quality 90)
  477.     {
  478.         return 'data:image/jpeg;base64,' base64_encode($this->get($quality));
  479.     }
  480.     /**
  481.      * Outputs the image
  482.      */
  483.     public function output($quality 90)
  484.     {
  485.         imagejpeg($this->contentsnull$quality);
  486.     }
  487.     /**
  488.      * @return array
  489.      */
  490.     public function getFingerprint()
  491.     {
  492.         return $this->fingerprint;
  493.     }
  494.     /**
  495.      * Returns a random number or the next number in the
  496.      * fingerprint
  497.      */
  498.     protected function rand($min$max)
  499.     {
  500.         if (!is_array($this->fingerprint)) {
  501.             $this->fingerprint = array();
  502.         }
  503.         if ($this->useFingerprint) {
  504.             $value current($this->fingerprint);
  505.             next($this->fingerprint);
  506.         } else {
  507.             $value mt_rand((int)$min, (int)$max);
  508.             $this->fingerprint[] = $value;
  509.         }
  510.         return $value;
  511.     }
  512.     /**
  513.      * @param $x
  514.      * @param $y
  515.      * @param $nw
  516.      * @param $ne
  517.      * @param $sw
  518.      * @param $se
  519.      *
  520.      * @return int
  521.      */
  522.     protected function interpolate($x$y$nw$ne$sw$se)
  523.     {
  524.         list($r0$g0$b0) = $this->getRGB($nw);
  525.         list($r1$g1$b1) = $this->getRGB($ne);
  526.         list($r2$g2$b2) = $this->getRGB($sw);
  527.         list($r3$g3$b3) = $this->getRGB($se);
  528.         $cx 1.0 $x;
  529.         $cy 1.0 $y;
  530.         $m0 $cx $r0 $x $r1;
  531.         $m1 $cx $r2 $x $r3;
  532.         $r  = (int) ($cy $m0 $y $m1);
  533.         $m0 $cx $g0 $x $g1;
  534.         $m1 $cx $g2 $x $g3;
  535.         $g  = (int) ($cy $m0 $y $m1);
  536.         $m0 $cx $b0 $x $b1;
  537.         $m1 $cx $b2 $x $b3;
  538.         $b  = (int) ($cy $m0 $y $m1);
  539.         return ($r << 16) | ($g << 8) | $b;
  540.     }
  541.     /**
  542.      * @param $image
  543.      * @param $x
  544.      * @param $y
  545.      *
  546.      * @return int
  547.      */
  548.     protected function getCol($image$x$y$background)
  549.     {
  550.         $L imagesx($image);
  551.         $H imagesy($image);
  552.         if ($x || $x >= $L || $y || $y >= $H) {
  553.             return $background;
  554.         }
  555.         return imagecolorat($image$x$y);
  556.     }
  557.     /**
  558.      * @param $col
  559.      *
  560.      * @return array
  561.      */
  562.     protected function getRGB($col)
  563.     {
  564.         return array(
  565.             (int) ($col >> 16) & 0xff,
  566.             (int) ($col >> 8) & 0xff,
  567.             (int) ($col) & 0xff,
  568.         );
  569.     }
  570.     /**
  571.      * Validate the background image path. Return the image type if valid
  572.      *
  573.      * @param string $backgroundImage
  574.      * @return string
  575.      * @throws Exception
  576.      */
  577.     protected function validateBackgroundImage($backgroundImage)
  578.     {
  579.         // check if file exists
  580.         if (!file_exists($backgroundImage)) {
  581.             $backgroundImageExploded explode('/'$backgroundImage);
  582.             $imageFileName count($backgroundImageExploded) > 1$backgroundImageExploded[count($backgroundImageExploded)-1] : $backgroundImage;
  583.             throw new Exception('Invalid background image: ' $imageFileName);
  584.         }
  585.         // check image type
  586.         $finfo finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
  587.         $imageType finfo_file($finfo$backgroundImage);
  588.         finfo_close($finfo);
  589.         if (!in_array($imageType$this->allowedBackgroundImageTypes)) {
  590.             throw new Exception('Invalid background image type! Allowed types are: ' join(', '$this->allowedBackgroundImageTypes));
  591.         }
  592.         return $imageType;
  593.     }
  594.     /**
  595.      * Create background image from type
  596.      *
  597.      * @param string $backgroundImage
  598.      * @param string $imageType
  599.      * @return resource
  600.      * @throws Exception
  601.      */
  602.     protected function createBackgroundImageFromType($backgroundImage$imageType)
  603.     {
  604.         switch ($imageType) {
  605.             case 'image/jpeg':
  606.                 $image imagecreatefromjpeg($backgroundImage);
  607.                 break;
  608.             case 'image/png':
  609.                 $image imagecreatefrompng($backgroundImage);
  610.                 break;
  611.             case 'image/gif':
  612.                 $image imagecreatefromgif($backgroundImage);
  613.                 break;
  614.             default:
  615.                 throw new Exception('Not supported file type for background image!');
  616.                 break;
  617.         }
  618.         return $image;
  619.     }
  620. }