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

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