twister/twister_captcha.php
<?php
/**
 * Twister Captcha
 */
class TwisterCaptcha {
  private
    $font_id = 0,
    $font_width = 0,
    $font_height = 0,
    $font_scale = 1,
    $font_spacing = 0,
    
    $difficulty = null,
    
    $char_dx_ratio = 0,
    $char_dy_ratio = 0,
    $noise_dy_ratio_min = 0,
    $noise_dy_ratio_max = 0,
    
    $blotFilter = IMG_FILTER_EDGEDETECT,
    
    $useScoreLines = false,
    $useGridLines = false,
    $useInkBlot = false,
    $useEdgeBlock = false,
    $useFakeCharPadding = false,
    $useFenceNoise = false,
    $useTopBottomY = false,
    $useInvert = false,
    $useStaticRot = false,
    $useOverlayId = 0,
    $useOverlayDark = true,
    $useAltBlackWhite = false,
    $useSimplexBg = false,
    $useEmboss = false,
    $useSpecialRot = false,
    $useExtraSpaces = false,
    $useEdgeDetect = false,
    $useMeanRemoval = false
  ;
  
  const CHAR_LIST = 'ADGHJKMNPRSTVWXY0248';
  const CHAR_LIST_SPLIT = 'ADGHJKMNTVWXY4';
  /**
   * B0 = dots
   * F7 = double ~
   * 1A / 1B = arrow right / left
   * A9 / AA = L rotated right / left
   */
  const CHAR_NOISE = "\xF7\x1A\x1B\xA9\xAA+<";
  
  const CHAR_NOISE_OVERLAY = "\xB0";
  
  const IMG_WIDTH_MAX = 300;
  const IMG_HEIGHT = 80;
  
  const SHEAR_X_MIN = 3;
  const SHEAR_X_MAX = 6;
  const SHEAR_Y_MIN = 14;
  const SHEAR_Y_MAX = 30;
  
  const SCALE_X_MIN = 0;
  const SCALE_X_MAX = 80;
  const SCALE_Y_MIN = 0;
  const SCALE_Y_MAX = 15;
  
  const SCALE_MAX = 80;
  
  const CHAR_DY_RATIO = 0.10;
  
  const NOISE_DY_RATIO_MIN = 0.380;
  const NOISE_DY_RATIO_MAX = 0.460;
  
  const NOISE_OFS_X_RATIO_MIN = 1.30;
  const NOISE_OFS_X_RATIO_MAX = 1.50;
  
  const LEVEL_EASY = 1;
  const LEVEL_NORMAL = 2;
  const LEVEL_HARD = 3;
  const LEVEL_LUNATIC = 4;
  
  const CHALLENGE_ID_BYTES = 12;
  
  // 32 bytes for SHA256
  private static $hmac_secret = '2Lm9j17D2SKElS1/s5SQCzIH1E031gSfKarbLNycerA=';
  
  public function __construct($font_file) {
    $font_id = imageloadfont($font_file);
    
    if ($font_id) {
      $this->font_id = $font_id;
      $this->font_width = imagefontwidth($font_id);
      $this->font_height = imagefontheight($font_id);
      
      $this->setFontScale(0.62);
      
      $this->setDyRatios(self::CHAR_DY_RATIO, self::NOISE_DY_RATIO_MIN, self::NOISE_DY_RATIO_MAX);
      
      $this->setDifficulty(self::LEVEL_NORMAL);
    }
  }
  
  public function __call($name, $args) {
    return false;
  }
  
  public function setDifficulty($val) {
    if ($val === $this->difficulty) {
      return true;
    }
    
    if ($val === self::LEVEL_EASY) {
      $this->char_dx_ratio = 0.210;
    }
    else if ($val === self::LEVEL_NORMAL) {
      $this->char_dx_ratio = 0.218;
    }
    else if ($val === self::LEVEL_HARD) {
      $this->char_dx_ratio = 0.240;
    }
    else if ($val === self::LEVEL_LUNATIC) {
      $this->char_dx_ratio = 0.280;
    }
    else {
      return false;
    }
    
    $this->difficulty = $val;
    
    return true;
  }
  
  public function setFontScale($scale) {
    if (!$scale || $scale <= 0) {
      $scale = 0.6;
    }
    
    $this->font_scale = self::IMG_HEIGHT / ($this->font_height * (100 + self::SHEAR_Y_MAX / 2) / 100) * $scale;
  }
  
  public function setDyRatios($char, $noise_min, $noise_max) {
    $char = (float)$char;
    $noise_min = (float)$noise_min;
    $noise_max = (float)$noise_max;
    
    if ($char <= 0.0 || $noise_min <= 0.0 || $noise_max <= 0.0) {
      return false;
    }
    
    $this->char_dy_ratio = $char;
    $this->noise_dy_ratio_min = $noise_min;
    $this->noise_dy_ratio_max = $noise_max;
  }
  
  private function debugOutputImage($img) {
    header('Content-Type: image/png');
    imagesavealpha($img, true);
    imagepng($img);
    die();
  }
  
  public function useJumpyMode($flag) {
    if ($flag) {
      $this->setDyRatios(0.5, 0.42, 0.5);
    }
    else {
      $this->setDyRatios(self::CHAR_DY_RATIO, self::NOISE_DY_RATIO_MIN, self::NOISE_DY_RATIO_MAX);
    }
  }
  
  public function useTopBottomY($flag) {
    $this->useTopBottomY = (bool)$flag;
  }
  
  public function useInvert($flag) {
    $this->useInvert = (bool)$flag;
  }
  
  public function useStaticRot($flag) {
    $this->useStaticRot = (bool)$flag;
  }
  
  public function useFenceNoise($flag) {
    $this->useFenceNoise = (bool)$flag;
  }
  
  public function useNegateBlotFilter($flag) {
    if ($flag === true) {
      $this->blotFilter = IMG_FILTER_NEGATE;
    }
    else {
      $this->blotFilter = IMG_FILTER_EDGEDETECT;
    }
  }
  
  public function useFakeCharPadding($flag) {
    $this->useFakeCharPadding = (bool)$flag;
  }
  
  public function useScoreLines($flag) {
    $this->useScoreLines = (bool)$flag;
  }
  
  public function useGridLines($flag) {
    $this->useGridLines = (bool)$flag;
  }
  
  public function useInkBlot($flag) {
    $this->useInkBlot = (bool)$flag;
  }
  
  public function useOverlayId($id, $dark = true) {
    $this->useOverlayId = (int)$id;
    $this->useOverlayDark = (bool)$dark;
  }
  
  public function useAltBlackWhite($flag) {
    $this->useAltBlackWhite = (bool)$flag;
  }
  
  public function useSimplexBg($flag) {
    $this->useSimplexBg = (bool)$flag;
  }
  
  public function useEmboss($flag) {
    $this->useEmboss = (bool)$flag;
  }
  
  public function useSpecialRot($flag) {
    $this->useSpecialRot = (bool)$flag;
  }
  
  public function useExtraSpaces($flag) {
    $this->useExtraSpaces = (bool)$flag;
  }
  
  public function useEdgeDetect($flag) {
    $this->useEdgeDetect = (bool)$flag;
  }
  
  public function useMeanRemoval($flag) {
    $this->useMeanRemoval = (bool)$flag;
  }
  
  public function useEdgeBlock($flag) {
    $this->useEdgeBlock = (bool)$flag;
    
    if ($this->useEdgeBlock) {
      $this->char_dx_ratio = 0.180;
    }
    else {
      $this->setDifficulty($this->difficulty);
    }
  }
  
  private function getRandomString($len) {
    $chars_lim = strlen(self::CHAR_LIST) - 1;
    
    $str = [];
    
    for ($i = 0; $i < $len; $i++) {
      $str[] = self::CHAR_LIST[mt_rand(0, $chars_lim)];
    }
    
    return implode('', $str);
  }
  
  private function getRandomStringPair($len, $offset = 1) {
    $chars_lim = strlen(self::CHAR_LIST) - 1;
    
    $str = [];
    $strike = [];
    
    for ($i = 0; $i < $len; $i++) {
      $k = mt_rand(0, $chars_lim);
      
      $str[] = self::CHAR_LIST[$k];
      
      $k += $offset;
      
      if ($k > $chars_lim) {
        $k = 0;
      }
      else if ($k < 0) {
        $k = $char_lim;
      }
      
      $strike[] = self::CHAR_LIST[$k];
    }
    
    return [ implode('', $str), implode('', $strike) ];
  }
  
  private function scaleImage($img, $w, $h) {
    $img_scaled = imagescale($img, $w, $h, IMG_NEAREST_NEIGHBOUR);
    return $img_scaled;
  }
  
  private function getScaleMatrix($x, $y) {
    return imageaffinematrixget(IMG_AFFINE_SCALE, [$x, $y]);
  }
  
  private function getShearHMatrix($a) {
    return imageaffinematrixget(IMG_AFFINE_SHEAR_HORIZONTAL, $a);
  }
  
  private function getShearVMatrix($a) {
    return imageaffinematrixget(IMG_AFFINE_SHEAR_VERTICAL, $a);
  }
  
  private function getRotMatrix($a) {
    return imageaffinematrixget(IMG_AFFINE_ROTATE, $a);
  }
  
  private function transformImage($img, $mat, $mat2 = null, $mat3 = null, $mat4 = null) {
    if ($mat2) {
      $mat = imageaffinematrixconcat($mat, $mat2);
    }
    
    if ($mat3) {
      $mat = imageaffinematrixconcat($mat, $mat3);
    }
    
    if ($mat4) {
      $mat = imageaffinematrixconcat($mat, $mat4);
    }
    
    $w = imagesx($img);
    $h = imagesy($img);
    
    $clip = ['x' => 0, 'y' => 0, 'width' => $w, 'height' => $h];
    
    imagesetinterpolation($img, IMG_NEAREST_NEIGHBOUR);
    
    $img = imageaffine($img, $mat, $clip);
    
    return $img;
  }
  
  private function warpImage($img, $scale_x, $rotate) {
    $shear_x = 
      [ 1, 0, (self::SHEAR_X_MIN + mt_rand(0, self::SHEAR_X_MAX)) * (mt_rand(0, 1) ? 1 : -1) / 100,
        1, 0, 0 ]
    ;
    
    $shear_y = 
      [ 1, (self::SHEAR_Y_MIN + mt_rand(0, self::SHEAR_Y_MAX)) * (mt_rand(0, 1) ? 1 : -1) / 100, 0,
        1, 0, 0 ]
    ;
    
    $scale = 
      [ $scale_x, 0, 0,
        1.0 + (mt_rand(self::SCALE_Y_MIN, self::SCALE_Y_MAX) / 100), 0, 0 ]
    ;
    
    if ($rotate) {
      if ($this->difficulty > TwisterCaptcha::LEVEL_HARD) {
        $da = 15;
      }
      else if ($this->difficulty > TwisterCaptcha::LEVEL_NORMAL) {
        $da = 11;
      }
      else {
        $da = 6;
      }
      
      $angle = mt_rand(-$da, $da) * M_PI / 180;
      
      $cos = cos($angle);
      $sin = sin($angle);
      
      $rot = [ $cos, -$sin, $sin, $cos, 0, 0 ];
      
      $mat = imageaffinematrixconcat($scale, $rot);
      $mat = imageaffinematrixconcat($mat, $shear_x);
    }
    else {
      $mat = imageaffinematrixconcat($scale, $shear_x);
    }
    
    $mat = imageaffinematrixconcat($mat, $shear_y);
    
    $img_warped = imageaffine($img, $mat);
    
    return [$img_warped, $scale_x];
  }
  
  private function cleanImageAlpha($img, $keep_true_color = false) {
    imagetruecolortopalette($img, false, 2);
    if ($keep_true_color) {
      imagepalettetotruecolor($img);
    }
    else {
      imagecolorset($img, 0, 0, 0, 0);
      imagecolorset($img, 1, 238, 238, 238);
    }
  }
  
  private function mergeCharImage($dest_img, $char_img, $dest_x, $dest_y, $clr, $alpha) {
    $w = imagesx($char_img);
    $h = imagesy($char_img);
    
    imagealphablending($dest_img, true);
    
    $txt_clr = imagecolorallocatealpha($dest_img, $clr, $clr, $clr, $alpha);
    
    for ($x = 0; $x < $w; ++$x) {
      for ($y = 0; $y < $h; ++$y) {
        $c = imagecolorat($char_img, $x, $y);
        
        $a = ($c >> 24) & 0xFF;
        $b = $c & 0xFF;
        
        if ($a < 1 && $b < 200) {
          imagesetpixel($dest_img, $dest_x + $x, $dest_y + $y, $txt_clr);
        }
      }
    }
    
    return true;
  }
  
  private function flattenImageAlpha($img) {
    imagetruecolortopalette($img, false, 3);
    
    for ($i = 0; $i < 3; $i++) { 
      $c = imagecolorsforindex($img, $i);
      
      if ($c['red'] < 128 && $c['green'] > 200) {
        imagecolortransparent($img, $i);
      }
      else if ($c['blue'] > 140) {
        imagecolorset($img, $i, 238, 238, 238);
      }
      else {
        imagecolorset($img, $i, 0, 0, 0);
      }
    }
  }
  
  private function prepareCharImage() {
    $img_char = imagecreate($this->font_width, $this->font_height);
    $img_char_clr_txt = imagecolorallocate($img_char, 0, 0, 0);
    $img_char_clr_bg = imagecolorallocate($img_char, 0, 255, 0);
    imagecolortransparent($img_char, $img_char_clr_bg);
    
    return [$img_char, $img_char_clr_txt, $img_char_clr_bg];
  }
  
  private function getCharImage($char, $height) {
    if ($height < 1) {
      return false;
    }
    
    $w = $this->font_width;
    $h = $this->font_height;
    
    // Create image
    $img = imagecreate($w, $h);
    $clr_bg = imagecolorallocate($img, 255, 255, 255);
    $clr_txt = imagecolorallocate($img, 0, 0, 0);
    imagealphablending($img, false);
    imagefilledrectangle($img, 0, 0, $w, $h, $clr_bg);
    
    // Draw character
    imagechar($img, $this->font_id, 1, 1, $char, $clr_txt);
    
    // Crop
    if ($char !== ' ') {
      $cropped = imagecropauto($img, IMG_CROP_WHITE);
      
      if ($cropped !== false) {
        $img = $cropped;
      }
    }
    
    // Resize
    $_r = imagesx($img) / imagesy($img);
    
    if ($_r <= 0) {
      return false;
    }
    
    $width = round($height * $_r);
    
    if ($width < 1) {
      return false;
    }
    //$this->debugOutputImage($img);
    
    $img = imagescale($img, $width, $height, IMG_NEAREST_NEIGHBOUR);
    
    return [$img, $width, $height];
  }
  
  private function drawCharFake($img, $img_char, $x, $y, $w, $h, $bg_clr, $char_clr) {
    $lim = strlen(self::CHAR_LIST_SPLIT) - 1;
    $char = self::CHAR_LIST_SPLIT[mt_rand(0, $lim)];
    
    $scale_x = 1.0 + (mt_rand(self::SCALE_X_MIN, self::SCALE_X_MAX) / 100);
    
    imagefilledrectangle($img_char, 0, 0, $this->font_width, $this->font_height, $bg_clr);
    imagechar($img_char, $this->font_id, 0, 0, $char, $char_clr);
    $img_scaled = $this->scaleImage($img_char, $w, $h);
    list($img_warped, $scale_x) = $this->warpImage($img_scaled, $scale_x, true);
    imagedestroy($img_scaled);
    $warped_width = imagesx($img_warped);
    $warped_height = imagesy($img_warped);
    $half_w_a = ceil($warped_width * 0.45);
    imagecopy($img, $img_warped,
      $x, $y,
      0, 0, $half_w_a, $warped_height
    );
    
    return round($w * $scale_x);
  }
  
  private function drawChar($img, $img_char, $char, $x, $y, $w, $h, $bg_clr, $char_clr,
    $scale_x = null, $rotate = false
  ) {
    imagefilledrectangle($img_char, 0, 0, $this->font_width, $this->font_height, $bg_clr);
    imagechar($img_char, $this->font_id, 0, 0, $char, $char_clr);
    
    if (!$scale_x) {
      $scale_x = 1.0 + (mt_rand(self::SCALE_X_MIN, self::SCALE_X_MAX) / 100);
    }
    else {
      $scale_x += mt_rand(0, self::SCALE_MAX) / 100;
    }
    
    $img_scaled = $this->scaleImage($img_char, $w, $h);
    
    list($img_warped, $scale_x) = $this->warpImage($img_scaled, $scale_x, $rotate);
    imagedestroy($img_scaled);
    
    $warped_width = imagesx($img_warped);
    $warped_height = imagesy($img_warped);
    $warped_delta_x = round(($warped_width - $w) / 2.0);
    
    imagecopy($img, $img_warped,
      $x, $y,
      0, 0, $warped_width, $warped_height
    );
    
    imagedestroy($img_warped);
    
    return round($w * $scale_x);
  }
  
  private function addNoise($img, $img_width, $img_height) {
    list($img_char, $img_char_clr_txt, $img_char_clr_bg) = $this->prepareCharImage();
    
    $char_width_scaled = round($this->font_width * $this->font_scale);
    $char_height_scaled = round($this->font_height * $this->font_scale);
    
    $noise_lim = strlen(self::CHAR_NOISE) - 1;
    
    $noise_dx = ceil($char_width_scaled * $this->char_dx_ratio);
    
    $noise_dy_min = floor($img_height * $this->noise_dy_ratio_min);
    $noise_dy_max = ceil($img_height * $this->noise_dy_ratio_max);
    
    $offset_x_min = round($char_width_scaled * self::NOISE_OFS_X_RATIO_MIN);
    $offset_x_max = round($char_width_scaled * self::NOISE_OFS_X_RATIO_MAX);
    
    $x = mt_rand($noise_dx, $char_width_scaled);
    $y = floor(($img_height - $char_height_scaled) / 2.0);
    
    $img_end = $img_width - $char_width_scaled;
    
    $y_flag = mt_rand(0, 1) ? true : false;
    
    while ($x < $img_end) {
      $this->drawChar($img, $img_char,
        self::CHAR_NOISE[mt_rand(0, $noise_lim)],
        $x + mt_rand(0, $noise_dx),
        $y + (mt_rand($noise_dy_min, $noise_dy_max)) * ($y_flag ? 1 : -1),
        $char_width_scaled, $char_height_scaled,
        $img_char_clr_bg, $img_char_clr_txt
      );
      
      $y_flag = !$y_flag;
      
      $x += mt_rand($offset_x_min, $offset_x_max);
    }
  }
  
  private function addLinesV($img, $img_width, $img_height, $line_count, $color) {
    $base_dx = (int)($img_width / ($line_count + 1));
    $min_dx = (int)($base_dx * 0.75);
    $max_dx = (int)($base_dx * 1.25);
    
    $min_dy = (int)($img_height / 5);
    $max_dy = (int)($img_height / 3);
    
    $x = mt_rand($base_dx, $max_dx);
    
    for ($i = 0; $i < $line_count; ++$i) {
      $this_x = $x + mt_rand($min_dx, $max_dx);
      
      $y1 = mt_rand($min_dy, $max_dy);
      $y2 = $img_height - mt_rand($min_dy, $max_dy);
      
      imageline($img,
        $this_x, $y1,
        $this_x, $y2,
        $color
      );
      
      $x += $base_dx;
    }
  }
  
  private function addPowder($img, $img_width, $img_height, $ratio, $color) {
    if ($this->difficulty > self::LEVEL_HARD) {
      $powder_max_width = 8;
    }
    else {
      $powder_max_width = 4;
    }
    
    $step = ceil(1 / $ratio);
    
    for ($x = 1; $x < $img_width; $x += $step) {
      for ($y = 1; $y < $img_height; $y += $step) {
        $x1 = mt_rand($x, $x + $step);
        $y1 = mt_rand($y, $y + $step);
        $size = mt_rand(1, $powder_max_width);
        imagefilledarc($img, $x1, $y1, $size, $size, 0, mt_rand(180, 360), $color, IMG_ARC_PIE);
      }
    }
  }
  
  private function addLines($img, $img_width, $img_height, $color) {
    $count = 5;
    
    $spacing = (int)($img_width / $count);
    $max_dx = (int)($img_width / 3);
    $mid_dx = ceil($max_dx / 2);
    
    $x = mt_rand(0, $max_dx);
    
    for ($i = 0; $i < $count; ++$i) {
      $dx = mt_rand(0, $max_dx);
      
      $x2 = $x + $dx;
      
      if ($dx >= $mid_dx) {
        imageline($img, $x, 0, $x2, $img_height, $color);
      }
      else {
        imageline($img, $x, $img_height, $x2, 0, $color);
      }
      
      $x += $spacing;
    }
  }
  
  private function addArc($img, $img_width, $img_height, $thick, $color) {
    $dx_min = 0;
    $dx_max = (int)($img_width * 0.35);
    
    $dy_min = $img_height;
    $dy_max = (int)($img_height * 1.8);
    
    $width_min = (int)($img_width) * 1.2;
    $width_max = (int)($img_width * 2.2);
    
    $start_x = (int)($img_width * 0.5);
    $start_y = (int)($img_height * 0.5);
    
    $alt = mt_rand(0, 1) === 1;
    
    $dy = mt_rand($dy_min, $dy_max);
    
    $x = $start_x + mt_rand($dx_min, $dx_max) * (mt_rand(0, 1) ? 1 : -1);
    
    if ($alt) {
      $y = $start_y - $dy;
    }
    else {
      $y = $start_y + $dy;
    }
    
    $alt = !$alt;
    
    $w = mt_rand($width_min, $width_max);
    $h = mt_rand((int)($dy * 1.7), (int)($dy * 2.3));
    
    imageellipse($img, $x, $y, $w, $h, $color);
    
    if ($thick) {
      imageellipse($img, $x, $y + 1, $w, $h, $color);
    }
  }
  
  private function addEdgeBlock($img, $img_width, $img_height) {
    $w = mt_rand(floor($img_width * 0.25), floor($img_width * 0.65));
    $x = mt_rand(0, $img_width - $w);
    //$w = $img_width;
    //$x = 0;
    
    $img_blot = imagecreatetruecolor($img_width, $img_height);
    imagecopy($img_blot, $img, 0, 0, 0, 0, $img_width, $img_height);
    imagefilter($img_blot, IMG_FILTER_EDGEDETECT);
    imagefilter($img_blot, IMG_FILTER_GAUSSIAN_BLUR);
    
    imagetruecolortopalette($img_blot, false, 8);
    
    for ($i = 0; $i < 8; $i++) { 
      $_c = imagecolorsforindex($img_blot, $i);
      
      if ($_c['green'] > 140) {
        imagecolorset($img_blot, $i, 0, 0, 0);
      }
      else {
        imagecolorset($img_blot, $i, 238, 238, 238);
      }
    }
    
    imagecopy($img, $img_blot, $x, 0, $x, 0, $w, $img_height);
    
    imagedestroy($img_blot);
    
    return [$x, $w];
  }
  
  private function addInkBlot($img, $img_width, $img_height) {
    $w = mt_rand(floor($img_width * 0.25), floor($img_width * 0.30));
    $x = mt_rand(0, $img_width - $w);
    $c_h = mt_rand(floor($img_height * 0.5), $img_height);
    
    $img_blot = imagecreatetruecolor($w, $img_height);
    imagecopy($img_blot, $img, 0, 0, $x, 0, $w, $img_height);
    imagefilter($img_blot, $this->blotFilter);
    
    $mask = imagecreatetruecolor($w, $img_height);
    $mask_green = imagecolorallocate($mask, 0, 255, 0);
    imagecolortransparent($mask, $mask_green);
    imagecopy($mask, $img, 0, 0, $x, 0, $w, $img_height);
    imagefilledellipse($mask, floor($w / 2), floor($img_height / 2), $w, $c_h, $mask_green);
    imagecopy($img_blot, $mask, 0, 0, 0, 0, $w, $img_height);
    
    imagecopy($img, $img_blot, $x, 0, 0, 0, $w, $img_height);
    
    imagedestroy($mask);
    imagedestroy($img_blot);
    
    return [$x, $w];
  }
  
  private function addScoreLines($img, $img_width, $img_height, $color) {
    $step = 10;
    
    $count = 10 + mt_rand(-2, 2);
    
    $x = mt_rand(floor($img_width * 0.10), floor($img_width * 0.20));
    $y = mt_rand(floor($img_height * 0.05), floor($img_height * 0.15));
    $l = mt_rand(floor($img_width * 0.60), floor($img_width * 0.70));
    
    for ($i = 0; $i < $count; $i++) {
      $j_x = mt_rand(-2, 2);
      $j_l = mt_rand(-2, 2);
      $y_j = mt_rand(-3, 3);
      imageline($img, $x + $j_x, $y, $x + $l + $j_l, $y + $y_j, $color);
      $y += $step;
    }
  }
  
  private function addGridLines($img, $img_width, $img_height, $color, $grid_size = 15, $line_width = 1) {
    if ($line_width > 1) {
      imagesetthickness($img, $line_width);
    }
    
    for ($x = mt_rand(2, $grid_size); $x < $img_width; $x += $grid_size) {
      imageline($img, $x + mt_rand(-1, 1), 0, $x + mt_rand(-1, 1), $img_height, $color);
    }
    
    for ($y = mt_rand(2, $grid_size); $y < $img_height; $y += $grid_size) {
      imageline($img, 0, $y + mt_rand(-1, 1), $img_width, $y + mt_rand(-1, 1), $color);
    }
    
    if ($line_width > 1) {
      imagesetthickness($img, 1);
    }
  }
  
  private function generateImage($str, $str_len) {
    if (!$this->font_id) {
      return false;
    }
    
    $char_width_scaled = round($this->font_width * $this->font_scale);
    $char_height_scaled = round($this->font_height * $this->font_scale);
    
    $char_dx = ceil($char_width_scaled * $this->char_dx_ratio);
    $char_dy = ceil($char_height_scaled * $this->char_dy_ratio);
    
    $max_possible_chars = ceil(self::IMG_WIDTH_MAX /
      ($char_width_scaled * (self::SCALE_X_MAX / 100 + 1) + $char_dx)
    );
    
    $pad_pool = $max_possible_chars - $str_len;
    /*
    if ($str_len > $max_possible_chars) {
      $ratio = $this->font_width / $this->font_height;
      $char_width_scaled = round(self::IMG_WIDTH_MAX / (self::SCALE_X_MAX / 100 + 1) / $str_len);
      $char_height_scaled = round($char_width_scaled / $ratio);
      $pad_pool = 0;
    }
    else {
      $pad_pool = $max_possible_chars - $str_len;
    }
    */ 
    $noise_lim = strlen(self::CHAR_NOISE) - 1;
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $x = mt_rand(0, $char_dx);
    
    if ($this->useTopBottomY) {
      $y = 0;
      $y_bot = $img_height - $char_height_scaled;
    }
    else {
      $y = $y_bot = ceil((($img_height - $char_height_scaled) / 2.0) - $char_height_scaled * 0.02);
    }
    
    $y_flag = (bool)mt_rand(0, 1);
    
    // Main image
    $img = imagecreatetruecolor($img_width, $img_height);
    $img_clr_bg = imagecolorallocate($img, 255, 255, 255);
    $img_clr_transp = imagecolorallocatealpha($img, 0, 255, 0, 127);
    
    imagealphablending($img, true);
    imagefill($img, 0, 0, $img_clr_bg);
    
    // Character image
    list($img_char, $img_char_clr_txt, $img_char_clr_bg) = $this->prepareCharImage();
    
    // Left padding
    $j = 0;
    
    while ($j < $pad_pool) {
      if (mt_rand(0, 1)) {
        $pad_pool--;
        
        $_y = $y + mt_rand(-$char_dy, $char_dy);
        
        if ($this->useFakeCharPadding) {
          $warped_width = $this->drawCharFake($img, $img_char,
            $x,
            $_y,
            $char_width_scaled, $char_height_scaled,
            $img_char_clr_bg, $img_char_clr_txt
          );
        }
        else {
          $warped_width = $this->drawChar($img, $img_char,
            self::CHAR_NOISE[mt_rand(0, $noise_lim)],
            $x,
            $_y,
            $char_width_scaled, $char_height_scaled,
            $img_char_clr_bg, $img_char_clr_txt
          );
        }
        
        $x += $warped_width;
      }
      else {
        $j++;
      }
    }

    // Characters
    $scale_x = 0;
    $scale_adjust = false;
    $scale_x_mid = (int)(self::SCALE_X_MAX - abs(self::SCALE_X_MIN)) / 2;
    
    $chars_start_x = $x;
    
    if ($this->difficulty > self::LEVEL_EASY && $str_len > 1) {
      $inter_pad_idx = mt_rand(1, $str_len - 1);
    }
    else {
      $inter_pad_idx = -1;
    }
    
    $bot_slot = mt_rand(0, $str_len - 1);
    
    for ($i = 0; $i < $str_len; $i++) {
      if ($i === $inter_pad_idx) {
        $x += $char_dx;
      }
      
      $this_y = $y;
      
      if ($this->useTopBottomY && $bot_slot === $i) {
        $this_y = $y_bot;
      }
      
      if ($scale_adjust) {
        if ($scale_x >= $scale_x_mid) {
          $scale_x = mt_rand(self::SCALE_X_MIN, $scale_x_mid);
          $scale_adjust = false;
        }
        else {
          $scale_x = mt_rand($scale_x_mid, self::SCALE_X_MAX);
          $scale_adjust = false;
        }
      }
      else {
        $scale_x = mt_rand(self::SCALE_X_MIN, self::SCALE_X_MAX);
        $scale_adjust = true;
      }
      
      $scale_x_f = 1.0 + ($scale_x / 100);
      
      $warped_width = $this->drawChar($img, $img_char,
        $str[$i],
        $x,
        $this_y + mt_rand(0, $char_dy) * ($y_flag ? 1 : -1),
        $char_width_scaled, $char_height_scaled,
        $img_char_clr_bg, $img_char_clr_txt, $scale_x_f, true
      );
      
      $y_flag = !$y_flag;
      
      $x += ($warped_width - $char_dx);
    }
    
    // Right padding
    $j = 0;
    
    while ($j < $pad_pool) {
      $pad_pool--;
      
      $_y = $y + mt_rand(-$char_dy, $char_dy);
      
      if ($this->useFakeCharPadding) {
        $warped_width = $this->drawCharFake($img, $img_char,
          $x,
          $_y,
          $char_width_scaled, $char_height_scaled,
          $img_char_clr_bg, $img_char_clr_txt
        );
      }
      else {
        $warped_width = $this->drawChar($img, $img_char,
          self::CHAR_NOISE[mt_rand(0, $noise_lim)],
          $x,
          $_y,
          $char_width_scaled, $char_height_scaled,
          $img_char_clr_bg, $img_char_clr_txt
        );
      }
      
      $x += $warped_width;
    }
    
    return [ $img, $img_width, $img_height, $img_clr_bg ];
  }
  
  private function generateBgImage($img_width) {
    if (!$this->font_id) {
      return false;
    }
    
    $char_width_scaled = round($this->font_width * $this->font_scale);
    $char_height_scaled = round($this->font_height * $this->font_scale);
    
    $char_dy = ceil($char_height_scaled * $this->char_dy_ratio);
    
    $img_height = self::IMG_HEIGHT;
    
    $_dx = ceil($char_width_scaled * $this->char_dx_ratio);
    $x = mt_rand($_dx, $char_width_scaled - $_dx);
    $y = floor((($img_height - $char_height_scaled) / 2.0) + $char_height_scaled * 0.025);
    $y_flag = (bool)mt_rand(0, 1);
    
    // Main image
    $img = imagecreatetruecolor($img_width, $img_height);
    $img_clr_bg = imagecolorallocate($img, 255, 255, 255);
    
    imagefilledrectangle($img, 0, 0, $img_width, $img_height, $img_clr_bg);
    
    // Character image
    list($img_char, $img_char_clr_txt, $img_char_clr_bg) = $this->prepareCharImage();
    
    $char_lim = strlen(self::CHAR_LIST) - 1;
    $x_lim = $img_width - $char_width_scaled;
    
    while ($x < $x_lim) {
      $warped_width = $this->drawChar($img, $img_char,
        self::CHAR_LIST[mt_rand(0, $char_lim)],
        $x,
        $y + $char_dy * ($y_flag ? 1 : -1),
        $char_width_scaled, $char_height_scaled,
        $img_char_clr_bg, $img_char_clr_txt
      );
      
      // ---
      
      $y_flag = !$y_flag;
      
      $x += $warped_width - mt_rand((int)($warped_width * 0.1), (int)($warped_width * 0.5));
    }
    
    return [ $img, $img_width, $img_height, $img_clr_bg ];
  }
  
  private function addFenceNoise($img, $img_width, $img_height, $img_clr_bg) {
    //$this->addPowder($img, $img_width, $img_height, 0.15, $img_clr_bg);
    $this->addGridLines($img, $img_width, $img_height, $img_clr_bg);
  }
  
  private function sliceVertical($img, $img_bg, $img_width, $img_height, $bg_width) {
    $img_clr_bg = imagecolorallocatealpha($img, 0, 255, 0, 127);
   
    $char_width = round($this->font_width * $this->font_scale);
    $char_height = round($this->font_height * $this->font_scale);
    
    $slice_width_min = floor($char_width * 0.8);
    $slice_width_max = ceil($char_width * 1.0);
    
    $slice_height_min = floor($img_height * 0.7);
    $slice_height_max = ceil($img_height * 0.9);
    
    $slice_dx = floor($slice_width_min / 10);
    $slice_dy = floor($char_height / 10);
    
    $slice_offset = $slice_width_min * 3;
    
    $img_start = $char_width + (int)($slice_width_min / 2);
    $img_end = $img_width - $char_width - 1;
    
    $fence_size = $img_end - $img_start;
        
    $bg_start = $img_start;
    $bg_end = $bg_width - $char_width - 1;
    
    $bg_fence_start = $bg_start + mt_rand(0, $bg_end - $fence_size - $bg_start);
    $bg_fence_end = $bg_fence_start + $fence_size - 1;
    
    $img_x = $img_start;
    $bg_x = $bg_fence_start;
    
    $slices = [];
    
    while ($img_x < $img_end) {
      $sw = mt_rand($slice_width_min, $slice_width_max);
      $sh = mt_rand($slice_height_min, $slice_height_max);
      
      $dx = mt_rand(-$slice_dx, $slice_dx);
      $sy = $img_height - $sh;
      
      imagecopy($img_bg, $img, $bg_x + $dx, $sy, $img_x + $dx, $sy, $sw, $sh);
      
      $slices[] = [ $img_x + $dx, $sy, $sw, $sh, $img_x + $sw - 2, $sh - 2 ];
      
      $img_x += $slice_offset;
      $bg_x += $slice_offset;
    }
    
    // Dummy fence
    $slice_lim = count($slices) - 1;
    
    $dummy_end = $bg_fence_start - $slice_width_min - 1;
    $dummy_x = $slice_width_min;
    
    while ($dummy_x < $dummy_end) {
      list($_x, $_y, $_w, $_h) = $slices[mt_rand(0, $slice_lim)];
      imagecopy($img_bg, $img, $dummy_x, $_y, $_x, $_y, $_w, $_h);
      $dummy_x += $slice_offset;
    }
    
    $dummy_end = $bg_end + $slice_width_min - 1;
    $dummy_x = $bg_fence_end + $slice_width_min;
    
    while ($dummy_x < $dummy_end) {
      list($_x, $_y, $_w, $_h) = $slices[mt_rand(0, $slice_lim)];
      imagecopy($img_bg, $img, $dummy_x, $_y, $_x, $_y, $_w, $_h);
      $dummy_x += $slice_offset;
    }
    
    // Add noise
    if ($this->difficulty > self::LEVEL_EASY && !$this->useTopBottomY) {
      $this->addNoise($img, $img_width, $img_height);
    }
    
    // Clear fence
    imagealphablending($img, false);
    
    foreach ($slices as $rect) {
      $width = $rect[4] - $rect[0];
      $height = $rect[5] - $rect[1];
      $x = $rect[0] + (int)($width / 2);
      $y = $rect[1] + (int)($height / 2);
      
      imagefilledrectangle($img,
        $rect[0], $rect[1],
        $rect[0] + $rect[2] - 1, $rect[1] + $rect[3] - 1,
      $img_clr_bg);
    }
    
    // Add transparent noise
    if ($this->useFenceNoise) {
      $this->addFenceNoise($img_bg, $bg_width, $img_height, 1);
    }
    
    imagealphablending($img, true);
    
    return $bg_fence_start - $bg_start;
  }
  
  private function sliceVerticalFog($img, $img_bg, $img_chars, $slide_range = 60) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $slice_width = ceil($this->font_width * $this->font_scale);
    
    if ($slice_width <= 0) {
      return false;
    }
    
    $this->removeBackground($img_chars);
    
    // Temp image containing background chars
    $img_chars_tmp = imagecreate($img_width, $img_height);
    imagepalettecopy($img_chars_tmp, $img_chars);
    imagecolortransparent($img_chars_tmp, 1);
    imagealphablending($img_chars_tmp, false);
    imagefilledrectangle($img_chars_tmp, 0, 0, $img_width, $img_height, 1);
    imagealphablending($img_chars_tmp, true);
    
    // Copy and clear 2 slices
    $_qw = ceil($img_width * 0.25);
    $slice_dx = mt_rand(0, $slide_range);
    $slices_x = [ $_qw - $slice_width, $img_width - $_qw - $slice_width ];
    
    $c = imagecolorallocatealpha($img, 0, 0, 0, 127);
    imagecolortransparent($img, $c);
    imagesavealpha($img, true);
    imagealphablending($img, false);
    
    $slice_mini_width = ceil($slice_width * 0.33);
    
    foreach ($slices_x as $x) {
      imagecopy($img_chars_tmp, $img_chars, $x + $slice_dx, 0, $x, 0, $slice_width, $img_height);
      
      for ($_i = 1; $_i < 4; $_i++) {
        $_x = $x + $slice_dx - $slice_mini_width * $_i;
        imagecopy($img_chars_tmp, $img_chars, $_x, 0, $x, 0, $slice_mini_width, $img_height);
        $_x = $x + $slice_dx + $slice_width + $slice_mini_width * $_i;
        imagecopy($img_chars_tmp, $img_chars, $_x, 0, $x + $slice_width - $slice_mini_width, 0, $slice_mini_width, $img_height);
      }
      
      imagefilledrectangle($img, $x, 0, $x + $slice_width - 1, $img_height - 1, $c);
    }
    
    //$this->addGridLines($img_chars_tmp, $img_width, $img_height, 0, mt_rand(15, 20));
    //$this->addPowder($img_chars_tmp, $img_width, $img_height, 0.15, 0);
    
    // Copy characters to background
    imagecolorset($img_chars_tmp, 0, 0, 0, 0, mt_rand(64, 72));
    imagealphablending($img_bg, true);
    imagecopy($img_bg, $img_chars_tmp, 0, 0, 0, 0, $img_width, $img_height);
    
    if ($this->useInvert) {
      imagefilter($img_bg, IMG_FILTER_NEGATE);
    }
    
    imagealphablending($img, true);
    
    return $slide_range;
  }
  
  private function sliceVerticalSimplex($img, $img_bg, $img_chars, $slide_range = 60) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $slice_width = ceil($this->font_width * $this->font_scale);
    
    if ($slice_width <= 0) {
      return false;
    }
    
    // Copy and clear 2 slices
    $_qw = ceil($img_width * 0.25);
    $slice_dx = mt_rand(0, $slide_range);
    $slices_x = [ $_qw - $slice_width, $img_width - $_qw - $slice_width ];
    
    $c = imagecolorallocatealpha($img, 0, 0, 0, 127);
    imagecolortransparent($img, $c);
    imagesavealpha($img, true);
    imagealphablending($img, false);
    
    imagealphablending($img_bg, true);
    
    $slice_mini_width = ceil($slice_width * 0.33);
    
    foreach ($slices_x as $x) {
      imagecopy($img_bg, $img_chars, $x + $slice_dx, 0, $x, 0, $slice_width, $img_height);
      
      for ($_i = 1; $_i < 4; $_i++) {
        $_x = $x + $slice_dx - $slice_mini_width * $_i;
        imagecopy($img_bg, $img_chars, $_x, 0, $x, 0, $slice_mini_width, $img_height);
        $_x = $x + $slice_dx + $slice_width + $slice_mini_width * $_i;
        imagecopy($img_bg, $img_chars, $_x, 0, $x + $slice_width - $slice_mini_width, 0, $slice_mini_width, $img_height);
      }
      
      imagefilledrectangle($img, $x, 0, $x + $slice_width - 1, $img_height - 1, $c);
    }
    
    // Noise
    $c = imagecolorallocatealpha($img, 255, 255, 255, mt_rand(85, 95));
    //$this->addGridLines($img_bg, $img_width, $img_height, $c, 30, 2);
    $this->addPowder($img_bg, $img_width, $img_height, 0.07, $c);
    
    if ($this->useInvert) {
      imagefilter($img_bg, IMG_FILTER_NEGATE);
    }
    
    imagealphablending($img, true);
    
    return $slide_range;
  }
  
  public function generateTwisterV($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    if ($char_count > 5) {
      $char_count = 5;
    }
    
    $challenge_str = $this->getRandomString($char_count);
    
    $this->setFontScale(0.96);
    
    list($img, $img_width, $img_height, $img_white) = $this->generateImage($challenge_str, $char_count);
    list($img_bg, $bg_width, $bg_height, $img_bg_white) = $this->generateBgImage($img_width + 59);
    
    $img_clr_bg = imagecolorallocatealpha($img, 0, 255, 0, 127);
    
    $clear_x = mt_rand(0, $bg_width - $img_width);
    $clear_h = floor($img_height * 0.4);
    $clear_y = $img_height - $clear_h;
    
    imagefilledrectangle($img_bg, 0, $clear_y, $bg_width, $img_height, $img_bg_white);
    imagecopy($img_bg, $img, $clear_x, $clear_y, 0, $clear_y, $img_width, $clear_h);
    imagefilledrectangle($img_bg, 0, $clear_y, $bg_width, $clear_y + 0, 0);
    
    imagefilter($img, IMG_FILTER_EDGEDETECT);
    
    // Rays
    $ray_count = mt_rand(18, 20);
    $spacing = ceil($img_width / ($ray_count + 1));
    $dx_max = 5;
    
    $x = mt_rand(0, $spacing);
    
    imagesetthickness($img, 2);
    
    for ($i = 0; $i < $ray_count; ++$i) {
      $x1 = $x;
      $y1 = 0;
      
      $to_x = $x1 + mt_rand(-$dx_max, $dx_max);
      
      for ($to_y = 0; $to_y < $clear_y; $to_y++) {
        $_c1 = imagecolorat($img, $to_x, $to_y);
        $_c2 = imagecolorat($img, $to_x + 1, $to_y);
        $_c3 = imagecolorat($img, $to_x - 1, $to_y);
        
        if ($_c1 === $img_white || $_c2 === $img_white || $_c3 === $img_white) {
          imageline($img, $x1, $y1, $to_x, $to_y, $img_white);
          $x += $spacing + mt_rand(0, 5);
          continue 2;
        }
      }
      
      imageline($img, $x1, $y1, $to_x, $to_y, $img_white);
      $x += $spacing + mt_rand(0, 5);
    }
    
    imagesetthickness($img, 1);
    
    // Grain
    $this->addPowder($img_bg, $bg_width, $img_height, 0.1, 0);
    
    $this->addPowder($img, $img_width, $img_height, 0.15, 0);
    
    // Moon
    $_x_min = ceil($img_width * 0.3);
    $_x_max = ceil($img_width * 0.7);
    imagefilledellipse($img,
      mt_rand($_x_min, $_x_max),
      mt_rand(-20, -15),
      300, 50,
    $img_white);
    
    // Clear
    imagealphablending($img, false);
    imagefilledrectangle($img, 0, $clear_y, $img_width, $img_height, $img_clr_bg);
    imagealphablending($img, true);
    
    $this->flattenImageAlpha($img);
    $this->cleanImageAlpha($img_bg);
    
    if ($this->useInvert) {
      imagecolorset($img, 0, 128, 128, 128);
      imagecolorset($img_bg, 0, 215, 215, 215);
      imagecolorset($img, 2, 255, 255, 255);
      imagecolorset($img_bg, 1, 128, 128, 128);
    }
    else {
      imagecolorset($img, 0, 108, 108, 108);
      imagecolorset($img_bg, 0, 108, 108, 108);
    }
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width, $fence_start ];
  }
  
  public function generateTwisterH($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    $challenge_str = $this->getRandomString($char_count);
    
    $background_str = $this->getRandomString($char_count + 2);
    
    list($img, $img_width, $img_height, $img_white) = $this->generateImage($challenge_str, $char_count);
    list($img_bg, $bg_width, $bg_height, $img_bg_white) = $this->generateBgImage($img_width + 49);
    
    if ($this->useEdgeBlock) {
      $this->addEdgeBlock($img, $img_width, $img_height);
    }
    
    if ($this->useInkBlot) {
      $this->addInkBlot($img, $img_width, $img_height);
    }
    
    if ($this->difficulty > self::LEVEL_EASY) {
      $this->addPowder($img, $img_width, $img_height, 0.08, 1);
    }
    
    if ($this->difficulty > self::LEVEL_EASY) {
      $this->addArc($img, $img_width, $img_height, false, 1);
    }
    
    $fence_start = $this->sliceVertical($img, $img_bg, $img_width, $img_height, $bg_width);
    
    if ($this->useScoreLines) {
      $this->addScoreLines($img, $img_width, $img_height, $clr_txt);
    }
    
    if ($this->useGridLines) {
      $this->addGridLines($img, $img_width, $img_height, $clr_txt);
    }
    
    if ($this->difficulty > self::LEVEL_EASY) {
      $this->addLines($img, $img_width, $img_height, $img_white);
    }
    
    $this->flattenImageAlpha($img);
    
    if ($this->difficulty > self::LEVEL_NORMAL) {
      $this->addPowder($img_bg, $bg_width, $img_height, 0.1, 1);
      $this->addPowder($img_bg, $bg_width, $img_height, 0.06, $img_bg_white);
      $this->addArc($img_bg, $bg_width, $img_height, false, $img_bg_white);
      $this->addArc($img_bg, $bg_width, $img_height, false, $img_bg_white);
    }
    else if ($this->difficulty > self::LEVEL_EASY) {
      $this->addPowder($img_bg, $bg_width, $img_height, 0.08, 1);
      $this->addPowder($img_bg, $bg_width, $img_height, 0.05, $img_bg_white);
      $this->addArc($img_bg, $bg_width, $img_height, false, $img_bg_white);
    }
    
    $this->cleanImageAlpha($img_bg);
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $bg_width, $fence_start ];
  }
  
  private function generateFogBg() {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $img = imagecreatetruecolor($img_width, $img_height);
    $img_clr_bg = imagecolorallocate($img, 255, 255, 255);
    imagefill($img, 0, 0, $img_clr_bg);
    
    $c_width_min = ceil($img_height * 1.8);
    $c_width_max = ceil($img_height * 2.5);
    
    $x_step = ceil($img_width / 5);
    
    if ($x_step <= 0) {
      $x_step = 10;
    }
    
    $x_jitter = ceil($img_width * 0.05);
    
    $x = 0;
    
    $top = (bool)mt_rand(0, 1);
    
    while ($x <= $img_width) {
      if ($top) {
        $_y = 0;
      }
      else {
        $_y = $img_height;
      }
      
      $_w = mt_rand($c_width_min, $c_width_max);
      $_h = mt_rand($c_width_min, $c_width_max);
      
      $_x = $x + mt_rand(-$x_jitter, $x_jitter);
      
      $gray = imagecolorallocatealpha($img, 0, 0, 0, mt_rand(95, 100));
      
      imagefilledellipse($img, $_x, $_y, $_w, $_h, $gray);
      
      $x += $x_step;
      
      $top = !$top;
    }
    
    return $img;
  }
  
  private function generateSimplexBg($zoom_min = 62, $zoom_max = 68) {
    $noise = new SimplexNoise();
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $img = imagecreatetruecolor($img_width, $img_height);
    
    $color_count = 8;
    
    $color_base = 64;
    
    $color_range = round((255 - $color_base * 2) / 6);
    
    $colors = [];
    
    for ($i = 0; $i < $color_count; $i++) {
      $c = $color_base + $i * $color_range + mt_rand(-8, 8);
      $colors[] = imagecolorallocate($img, $c, $c, $c);
    }
    
    $zoom = mt_rand($zoom_min, $zoom_max);
    
    $_cc = $color_count - 1;
    
    for ($x = 0; $x < $img_width; ++$x) {
      for ($y = 0; $y < $img_height; ++$y) {
        $n = $noise->noise($x / $zoom, $y / $zoom);
        $c = floor((1 + $n) / 2 * $_cc);
        if ($c > $_cc) {
          $c = $_cc;
        }
        $ci = $colors[$c];
        imagesetpixel($img, $x, $y, $ci);
      }
    }
    
    return $img;
  }
  
  private function removeBackground($img) {
    $this->cleanImageAlpha($img);
    imagecolortransparent($img, 1);
  }
  
  private function overlayPattern($img, $type, $dark = true) {
    if ($type === 1) {
      $this->overlayPatternRings($img, $dark);
    }
    else if ($type === 2) {
      $this->overlayPatternRad($img, $dark);
    }
    else if ($type === 3) {
      $this->overlayPatternMoz($img, $dark);
    }
    else if ($type === 4) {
      $this->overlayPatternSimplexA($img, 16, 120);
    }
    else if ($type === 5) {
      $this->overlayPatternHatchA($img, 10, 2);
    }
    else if ($type === 6) {
      $this->overlayPatternSimplexA($img, 1, 110);
    }
  }
  
  private function overlayPatternHatchA($img, $noise_width = 10, $zoom = 2) {
    $noise = new SimplexNoise();
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $img_noise = imagecreatetruecolor($noise_width, $img_height);
    
    $c1 = imagecolorallocatealpha($img_noise, 0, 0, 0, 110);
    $c2 = imagecolorallocatealpha($img_noise, 255, 255, 255, 110);
    
    imagealphablending($img_noise, false);
    
    for ($x = 0; $x < $img_width; ++$x) {
      for ($y = 0; $y < $img_height; ++$y) {
        $n = $noise->noise($x / $zoom, $y / $zoom);
        $c = floor((1 + $n) / 2 * 255);
        if ($c < 128) {
          $c = $c1;
        }
        else {
          $c = $c2;
        }
        imagesetpixel($img_noise, $x, $y, $c);
      }
    }
    
    $img_noise = imagescale($img_noise, $img_width, $img_height);
    
    //$this->debugOutputImage($img_noise);
    
    imagealphablending($img, true);
    
    imagecopy($img, $img_noise, 0, 0, 0, 0, $img_width, $img_height);
  }
  
  private function overlayPatternSimplexA($img, $zoom = 16, $alpha = 120) {
    $noise = new SimplexNoise();
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    if ($alpha < 0) {
      $alpha = 0;
    }
    else if ($alpha > 127) {
      $alpha = 127;
    }
    
    $img_noise = imagecreatetruecolor($img_width, $img_height);
    
    $c1 = imagecolorallocatealpha($img_noise, 0, 0, 0, $alpha);
    $c2 = imagecolorallocatealpha($img_noise, 255, 255, 255, $alpha);
    
    imagealphablending($img_noise, false);
    
    if ($zoom <= 0) {
      $zoom = 1;
    }
    
    for ($x = 0; $x < $img_width; ++$x) {
      for ($y = 0; $y < $img_height; ++$y) {
        $n = $noise->noise($x / $zoom, $y / $zoom);
        $c = floor((1 + $n) / 2 * 255);
        if ($c < 128) {
          $c = $c1;
        }
        else {
          $c = $c2;
        }
        imagesetpixel($img_noise, $x, $y, $c);
      }
    }
    
    //$this->debugOutputImage($img_noise);
    
    imagealphablending($img, true);
    
    imagecopy($img, $img_noise, 0, 0, 0, 0, $img_width, $img_height);
  }
  
  private function overlayPatternRings($img, $dark = true) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    if ($dark) {
      $c_rings = imagecolorallocatealpha($img, 0, 0, 0, 110);
    }
    else {
      $c_rings = imagecolorallocatealpha($img, 255, 255, 255, 110);
    }
    
    imagealphablending($img, true);
    
    $ring_w = mt_rand(ceil($img_width * 0.06), ceil($img_width * 0.08));
    $ring_d = ceil($ring_w * 0.5) + 1;
    
    $x_max = $img_width + $ring_w;
    $y_max = $img_height + $ring_w;
    
    for ($x = mt_rand(1 - $ring_w, 0); $x < $x_max; $x += $ring_d) {
      for ($y = mt_rand(1 - $ring_w, 0); $y < $y_max; $y += $ring_d) {
        imageellipse($img, $x, $y, $ring_w, $ring_w, $c_rings);
      }
    }
  }
  
  private function overlayPatternRad($img, $dark = true) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    if ($dark) {
      $c_rings = imagecolorallocatealpha($img, 0, 0, 0, 110);
    }
    else {
      $c_rings = imagecolorallocatealpha($img, 255, 255, 255, 110);
    }
    
    imagealphablending($img, true);
    
    $x = mt_rand(round($img_width * 0.4), round($img_width * 0.6));
    $y = mt_rand(round($img_height * 0.4), round($img_height * 0.6));
    $w_d = 16;
    
    $w = $w_d;
    
    while ($w < $img_width) {
      imageellipse($img, $x, $y, $w, $w, $c_rings);
      imageellipse($img, $x, $y, $w + 2, $w + 2, $c_rings);
      $w += $w_d;
    }
  }
  
  private function overlayPatternMoz($img, $dark = true) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    imagealphablending($img, true);
    
    $color_map = [];
    
    $sq_w = 6;
    
    for ($x = 0; $x < $img_width; $x += $sq_w) {
      for ($y = 0; $y < $img_height; $y += $sq_w) {
        $_c = mt_rand(100, 115);
        
        if (isset($color_map[$_c])) {
          $c_rings = $color_map[$_c];
        }
        else {
          if ($dark) {
            $c_rings = imagecolorallocatealpha($img, 0, 0, 0, $_c);
          }
          else {
            $c_rings = imagecolorallocatealpha($img, 255, 255, 255, $_c);
          }
          
          $color_map[$_c] = $c_rings;
        }
        
        imagefilledrectangle($img, $x, $y, $x + $sq_w - 1, $y + $sq_w - 1, $c_rings);
      }
    }
  }
  
  private function applyTextToFogBg($img_chars, $img_bg) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $this->cleanImageAlpha($img_chars);
    
    imagealphablending($img_bg, true);
    
    $color_map = [];
    
    $c_text = imagecolorallocatealpha($img_bg, 0, 0, 0, mt_rand(70, 80));
    $c_text_alt = imagecolorallocatealpha($img_bg, 255, 255, 255, mt_rand(40, 50));
    
    if ($this->useInvert) {
      $_c = $c_text;
      $c_text = $c_text_alt;
      $c_text_alt = $_c;
    }
    
    $anti_x1 = mt_rand(round($img_width * 0.30), round($img_width * 0.45));
    $anti_x2 = mt_rand(round($img_width * 0.55), round($img_width * 0.60));
    
    $anti_x1_jitter = $anti_x1 - 2;
    $anti_x2_jitter = $anti_x2 + 2;
    
    for ($x = 0; $x < $img_width; ++$x) {
      for ($y = 0; $y < $img_height; ++$y) {
        $c_chars = imagecolorat($img_chars, $x, $y);
        
        // Character pixel, bright if inside the anti rect, dark otherwise
        if ($c_chars === 0) {
          $_anti = false;
          
          if ($this->useInkBlot) {
            if ($x >= $anti_x1 && $x <= $anti_x2) {
              $_anti = true;
            }
            else if ($x >= $anti_x1_jitter && $x < $anti_x1) {
              $_anti = !!mt_rand(0, 1);
            }
            else if ($x > $anti_x2 && $x <= $anti_x2_jitter) {
              $_anti = !!mt_rand(0, 1);
            }
          }
          
          if ($_anti) {
            imagesetpixel($img_bg, $x, $y, $c_text_alt);
          }
          else {
            imagesetpixel($img_bg, $x, $y, $c_text);
          }
        }
        
        $c_delta = mt_rand(100, 120);
        
        if (isset($color_map[$c_delta])) {
          $c_noise = $color_map[$c_delta];
        }
        else {
          $c_noise = imagecolorallocatealpha($img_bg, 0, 0, 0, $c_delta);
        }
        
        $color_map[$c_delta] = $c_noise;
        
        imagesetpixel($img_bg, $x, $y, $c_noise);
      }
    }
  }
    
  private function applyTextToSimplexBg($img_chars, $img_bg, $dirx = 2, $diry = 2) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    imagealphablending($img_bg, true);
    
    imagecolorset($img_chars, 0, 255, 255, 255, mt_rand(60, 70));
    imagecopy($img_bg, $img_chars, 0, 0, 0, 0, $img_width, $img_height);
    
    imagecolorset($img_chars, 0, 0, 0, 0, mt_rand(70, 80));
    imagecopy($img_bg, $img_chars, $dirx, $diry, 0, 0, $img_width, $img_height);
  }
  
  public function generateStatic($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    if ($this->useStaticRot) {
      $this->setFontScale(0.5);
    }
    
    $challenge_str = $this->getRandomString($char_count);
    
    list($img, $img_width, $img_height, $clr_bg) = $this->generateImage($challenge_str, $char_count);
    
    if (!$this->useTopBottomY) {
      $this->addNoise($img, $img_width, $img_height);
    }
    
    if ($this->useStaticRot) {
      $img = imagerotate($img, 15, $clr_bg);
      
      $_w = imagesx($img);
      $_h = imagesy($img);
      $_x = round(abs($_w - $img_width) * 0.5);
      $_y = round(abs($_h - $img_height) * 0.65);
      
      $img = imagecrop($img, ['x' => $_x, 'y' => $_y, 'width' => $img_width, 'height' => $img_height]);
    }
    else {
      // Overlay noise
      if ($this->difficulty > self::LEVEL_EASY) {
        list($img_char, $img_char_clr_txt, $img_char_clr_bg) = $this->prepareCharImage();
        
        $char_width_scaled = $this->font_width * 2.4;
        $char_height_scaled = $this->font_height * 2.4;
        
        $noise_count = ceil($char_count / 2);
        
        $dx = (int)($img_width / $noise_count);
        
        $x = mt_rand($char_width_scaled, $dx);
        
        for ($i = 0; $i < $noise_count; ++$i) {
          $this->drawChar($img, $img_char,
            self::CHAR_NOISE_OVERLAY[0],
            $x,
            (int)($img_height / 2) - (int)($char_height_scaled / 2),
            $char_width_scaled, $char_height_scaled,
            $img_char_clr_bg, $img_char_clr_txt
          );
          
          $x += mt_rand($char_width_scaled, $dx);
        }
      }
    }
    
    $this->cleanImageAlpha($img);
    
    if ($this->difficulty > self::LEVEL_EASY) {
      $this->addPowder($img, $img_width, $img_height, 0.08, 0);
      if (!$this->useStaticRot) {
        $this->addPowder($img, $img_width, $img_height, 0.06, 1);
      }
    }
    
    return [ $challenge_str, $img, $img_width, $img_height ];
  }
  
  private function applyNoiseToFog($img) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $img_noise = imagecreatetruecolor($img_width, $img_height);
    $img_noise_clr_bg = imagecolorallocate($img_noise, 255, 255, 255);
    $img_noise_clr_transp = imagecolorallocatealpha($img_noise, 0, 255, 0, 127);
    imagefill($img_noise, 0, 0, $img_noise_clr_bg);
    
    $this->addNoise($img_noise, $img_width, $img_height);
    
    $this->cleanImageAlpha($img_noise);
    
    imagecolortransparent($img_noise, 1);
    
    $_g = mt_rand(40, 80);
    imagecolorset($img_noise, 0, $_g, $_g, $_g);
    
    imagecopy($img, $img_noise, 0, 0, 0, 0, $img_width, $img_height);
  }
  
  public function generateSimpleTask($task_id = 0) {
    if (!$this->font_id) {
      return false;
    }
    
    $min_size = 24;
    $max_size = 32;
    
    $visual_count = 10;
    $visual_str = $this->getRandomString($visual_count);
    
    if ($task_id < 1 || $task_id > 2) {
      $task_id = mt_rand(1, 2);
    }
    
    $char_count = mt_rand(4, 6);
    
    $task_str_2 = '';
    
    // Type first or last characters
    if ($task_id == 1) {
      $_ary = [
        3 => 'three',
        4 => 'four',
        5 => 'five',
        6 => 'six',
        7 => 'seven'
      ];
      
      if (isset($_ary[$char_count]) && mt_rand(0, 1)) {
        $_char_count_str = $_ary[$char_count];
      }
      else {
        $_char_count_str = $char_count;
      }
      
      if (mt_rand(0, 1)) {
        $challenge_str = substr($visual_str, 0, $char_count);
        $task_str_1 = ('only type the first ' . $_char_count_str . ' characters');
      }
      else {
        $challenge_str = substr($visual_str, -$char_count);
        $task_str_1 = ('only type the last ' . $_char_count_str . ' characters');
      }
    }
    // Type characters with symbol above
    else if ($task_id == 2) {
      $task_str_1 = 'only type characters';
      $task_str_2 = ' with a circle above them';
      
      $picked_char_ids = array_rand(str_split($visual_str), $char_count);
      
      $challenge_str = '';
      
      foreach ($picked_char_ids as $key) {
        $challenge_str .= $visual_str[$key];
      }
    }
    
    // ---
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $img_half_height = round($img_height * 0.5);
    
    if ($this->useSimplexBg) {
      $img = $this->generateSimplexBg();
    }
    else {
      $img = $this->generateFogBg();
    }
    
    // ---
    
    $task_str_1_x = mt_rand(0, 20);
    
    if ($task_str_2) {
      $task_str_1_y = mt_rand(0, 2);
    }
    else {
      $task_str_1_y = mt_rand(0, 8);
    }
    
    imagestring($img, $this->font_id, $task_str_1_x, $task_str_1_y, $task_str_1, 0);
    
    if ($task_str_2) {
      $task_str_2_x = $task_str_1_x + mt_rand(20, 40);
      $task_str_2_y = $task_str_1_y + 12;
      imagestring($img, $this->font_id, $task_str_2_x, $task_str_2_y, $task_str_2, 0);
    }
    
    // ---
    
    $char_imgs = [];
    
    $x = 0;
    
    for ($i = 0; $i < $visual_count; $i++) {
      $_c = $visual_str[$i];

      list($char) = $this->getCharImage($_c, mt_rand($min_size, $max_size));
      
      if ($this->useSpecialRot) {
        $mat_rot = $this->getRotMatrix(-mt_rand(45, 65));
        $mat_shear_h = null;
        $mat_shear_v = null;
      }
      else {
        $mat_rot = $this->getRotMatrix(mt_rand(-25, 25));
        $mat_shear_h = $this->getShearHMatrix(mt_rand(-12, 12));
        $mat_shear_v = $this->getShearVMatrix(mt_rand(-25, 25));
      }
      
      $char = $this->transformImage($char, $mat_rot, $mat_shear_v, $mat_shear_h);
      
      $char_w = imagesx($char);
      $char_h = imagesy($char);
      
      $y = $img_half_height + 2;
      
      if ($this->useAltBlackWhite && mt_rand(0, 1)) {
        $c = 255;
        $alpha = mt_rand(55, 65);
      }
      else {
        $c = 0;
        $alpha = mt_rand(55, 75);
      }
      
      $char_imgs[] = [$char, $char_w, $char_h, $x, $y, $c, $alpha];
      
      $dx = mt_rand(-1, 2);
      
      $x += $char_w + $dx;
    }
    
    $x0 = max(0, $img_width - $x);
    $x0 = round($x0 * 0.5);
    
    foreach ($char_imgs as $_char) {
      list($char, $char_w, $char_h, $x, $y, $c, $alpha) = $_char;
      $this->mergeCharImage($img, $char, $x0 + $x, $y, $c, $alpha);
      
      if ($this->useEmboss) {
        $this->mergeCharImage($img, $char, $x0 + $x + 2, $y - 2, $c ? 0 : 255, $alpha);
      }
    }
    
    if ($this->useInvert) {
      imagefilter($img, IMG_FILTER_NEGATE);
    }
    
    $img_bg = $this->generateFogBg();
    
    $bg_chars = $this->generateSimplexBg();
    
    foreach ($char_imgs as $_char) {
      list($char, $char_w, $char_h, $x, $y, $c, $alpha) = $_char;
      $this->mergeCharImage($bg_chars, $char, $x0 + $x, $y, $c, $alpha);
      
      if ($this->useEmboss) {
        $this->mergeCharImage($bg_chars, $char, $x0 + $x + 2, $y - 2, $c ? 0 : 255, $alpha);
      }
    }
    
    imagestring($bg_chars, $this->font_id, $task_str_1_x, $task_str_1_y, $task_str_1, 0);
    
    if ($task_str_2) {
      imagestring($bg_chars, $this->font_id, $task_str_2_x, $task_str_2_y, $task_str_2, 0);
    }
    
    if ($this->useEdgeDetect && !$this->useEmboss) {
      $_b = 25;
      $_c = 20;
      
      imagefilter($img, IMG_FILTER_EDGEDETECT);
      imagefilter($bg_chars, IMG_FILTER_EDGEDETECT);
      
      imagefilter($img, IMG_FILTER_BRIGHTNESS, mt_rand(-$_b, $_b));
      imagefilter($img, IMG_FILTER_CONTRAST, mt_rand(-$_c, $_c));
      
      imagefilter($bg_chars, IMG_FILTER_BRIGHTNESS, mt_rand(-$_b, $_b));
      imagefilter($bg_chars, IMG_FILTER_CONTRAST, mt_rand(-$_c, $_c));
    }
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    if ($this->useGridLines) {
      $_c = imagecolorallocatealpha($img, 255, 255, 255, 105);
      $this->addGridLines($img, $img_width, $img_height, $_c, mt_rand(15, 20), 2);
    }
    
    $slider_range = $this->sliceVerticalSimplex($img, $img_bg, $bg_chars);
    
    // Print symbols for task 2
    if ($task_id == 2) {
      $_i = 0;
      
      foreach ($picked_char_ids as $key) {
        $_char = $char_imgs[$key];
        
        list($char, $char_w, $char_h, $x, $y, $c, $alpha) = $_char;
        
        $_dot_d = mt_rand(8, 10);
        
        if (mt_rand(0, 1)) {
          $_c = imagecolorallocatealpha($img, 255, 255, 255, 50);
        }
        else {
          $_c = imagecolorallocatealpha($img, 0, 0, 0, 50);
        }
        
        imagefilledellipse($img, $x0 + $x + round($char_w * 0.5) - 4, $y - mt_rand(7, 9), $_dot_d, $_dot_d, $_c);
        
        $_i++;
      }
    }
    
    //$this->debugOutputImage($char);
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $img_width + $slider_range, 0 ];
  }
  
  public function generateTwisterHFogNew($char_count, $min_size = 38, $max_size = 52) {
    if (!$this->font_id) {
      return false;
    }
    
    if (!$min_size || $min_size < 0) {
      $min_size = 38;
    }
    
    if (!$max_size || $max_size < 0) {
      $max_size = 52;
    }
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $challenge_str = $this->getRandomString($char_count);
    
    if ($this->useSimplexBg) {
      $img = $this->generateSimplexBg();
    }
    else {
      $img = $this->generateFogBg();
    }
    
    $bleach = imagecolorallocatealpha($img, 255, 255, 255, mt_rand(80, 100));
    $this->addPowder($img, $img_width, $img_height, 0.08, $bleach);
    
    $char_imgs = [];
    
    $x = 0;
    
    if ($this->useExtraSpaces) {
      $_extra_space_i = (int)($char_count * 0.5) + mt_rand(-1, 1);
    }
    
    for ($i = 0; $i < $char_count; $i++) {
      $_c = $challenge_str[$i];

      list($char) = $this->getCharImage($_c, mt_rand($min_size, $max_size));
      
      if ($this->useSpecialRot) {
        $mat_rot = $this->getRotMatrix(-mt_rand(45, 65));
        $mat_shear_h = null;
        $mat_shear_v = null;
      }
      else {
        $mat_rot = $this->getRotMatrix(mt_rand(-30, 30));
        $mat_shear_h = $this->getShearHMatrix(mt_rand(-15, 15));
        $mat_shear_v = $this->getShearVMatrix(mt_rand(-30, 30));
      }
      
      $char = $this->transformImage($char, $mat_rot, $mat_shear_v, $mat_shear_h);
      
      $char_w = imagesx($char);
      $char_h = imagesy($char);
      
      if ($img_height >= $char_h) {
        $y = mt_rand(0, $img_height - $char_h);
      }
      else {
        $y = $img_height - $char_h;
      }
      
      if ($this->useAltBlackWhite && mt_rand(0, 1)) {
        $c = 255;
        $alpha = mt_rand(55, 65);
      }
      else {
        $c = 0;
        $alpha = mt_rand(55, 75);
      }
      
      $char_imgs[] = [$char, $char_w, $char_h, $x, $y, $c, $alpha];
      
      $dx = mt_rand(-10, 2);
      
      $x += $char_w + $dx;
      
      if ($this->useExtraSpaces && $i == $_extra_space_i) {
        $x += $char_w + mt_rand(0, $char_w);
      }
    }
    
    $x0 = max(0, $img_width - $x);
    $x0 = round($x0 * 0.5);
    
    foreach ($char_imgs as $_char) {
      list($char, $char_w, $char_h, $x, $y, $c, $alpha) = $_char;
      $this->mergeCharImage($img, $char, $x0 + $x, $y, $c, $alpha);
      
      if ($this->useEmboss) {
        $this->mergeCharImage($img, $char, $x0 + $x + 2, $y - 2, $c ? 0 : 255, $alpha);
      }
    }
    
    if ($this->useInvert) {
      imagefilter($img, IMG_FILTER_NEGATE);
    }
    
    $img_bg = $this->generateFogBg();
    
    $bg_chars = $this->generateSimplexBg();
    
    foreach ($char_imgs as $_char) {
      list($char, $char_w, $char_h, $x, $y, $c, $alpha) = $_char;
      $this->mergeCharImage($bg_chars, $char, $x0 + $x, $y, $c, $alpha);
      
      if ($this->useEmboss) {
        $this->mergeCharImage($bg_chars, $char, $x0 + $x + 2, $y - 2, $c ? 0 : 255, $alpha);
      }
    }
    
    if ($this->useEdgeDetect && !$this->useEmboss) {
      $_b = 25;
      $_c = 20;
      
      imagefilter($img, IMG_FILTER_EDGEDETECT);
      imagefilter($bg_chars, IMG_FILTER_EDGEDETECT);
      
      imagefilter($img, IMG_FILTER_BRIGHTNESS, mt_rand(-$_b, $_b));
      imagefilter($img, IMG_FILTER_CONTRAST, mt_rand(-$_c, $_c));
      
      imagefilter($bg_chars, IMG_FILTER_BRIGHTNESS, mt_rand(-$_b, $_b));
      imagefilter($bg_chars, IMG_FILTER_CONTRAST, mt_rand(-$_c, $_c));
    }
    
    if ($this->useMeanRemoval && !$this->useEdgeDetect) {
      imagefilter($img, IMG_FILTER_MEAN_REMOVAL);
      imagefilter($bg_chars, IMG_FILTER_MEAN_REMOVAL);
    }
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    if ($this->useGridLines) {
      $_c = imagecolorallocatealpha($img, 255, 255, 255, 105);
      $this->addGridLines($img, $img_width, $img_height, $_c, mt_rand(15, 20), 2);
    }
    
    $slider_range = $this->sliceVerticalSimplex($img, $img_bg, $bg_chars);
    
    //$this->debugOutputImage($char);
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $img_width + $slider_range, 0 ];
  }
  
  public function generateStaticFog($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $challenge_str = $this->getRandomString($char_count);
    
    // Background image
    $img = $this->generateFogBg();
    
    // Text image
    list($img_chars) = $this->generateImage($challenge_str, $char_count);
    
    // Transparent line
    $this->addArc($img_chars, $img_width, $img_height, false, 1);
    
    // Copy the text to the bg
    $this->applyTextToFogBg($img_chars, $img);
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    if ($this->useScoreLines) {
      $this->addScoreLines($img, $img_width, $img_height, 1);
    }
    
    if ($this->useGridLines) {
      $this->addGridLines($img, $img_width, $img_height, 1);
    }
    
    // Top / Bottom symbol noise
    $this->applyNoiseToFog($img);
    
    // ---
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, self::IMG_WIDTH_MAX, self::IMG_HEIGHT ];
  }
  
  public function generateTwisterHFog($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $challenge_str = $this->getRandomString($char_count);
    
    // Foreground fog
    $img = $this->generateFogBg();
    
    //$bleach = imagecolorallocatealpha($img, 255, 255, 255, mt_rand(80, 100));
    //$grid_size = mt_rand(floor($img_height * 0.35), floor($img_height * 0.55));
    //$this->addGridLines($img, $img_width, $img_height, $bleach, $grid_size, 2);
    //$this->addPowder($img, $img_width, $img_height, 0.08, $bleach);
    
    // Background fog
    $img_bg = $this->generateFogBg();
    
    // Text image
    list($img_chars_orig) = $this->generateImage($challenge_str, $char_count);
    $img_chars = $this->cloneTextImage($img_chars_orig);
    
    $this->removeBackground($img_chars);
    
    // Copy the text to the main fog image
    $this->applyTextToFogBg($img_chars, $img);
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    // Slice
    $slider_range = $this->sliceVerticalFog($img, $img_bg, $img_chars_orig);
    
    // Perturbations for the character image
    $this->addArc($img_chars, $img_width, $img_height, false, 1);
    
    if ($this->useScoreLines) {
      $this->addScoreLines($img, $img_width, $img_height, 1);
    }
    
    if ($this->useGridLines) {
      $this->addGridLines($img, $img_width, $img_height, 1);
    }
    
    if (!$this->useTopBottomY) {
      $this->applyNoiseToFog($img);
    }
    
    // ---
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $img_width + $slider_range, 0 ];
  }
  
  public function generateStaticSimplex($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    $challenge_str = $this->getRandomString($char_count);
    
    // Background image
    $img = $this->generateSimplexBg();
    
    // Text image
    list($img_chars) = $this->generateImage($challenge_str, $char_count);
    
    $this->removeBackground($img_chars);
    
    $this->applyTextToSimplexBg($img_chars, $img, -2, 2);
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    if ($this->useInvert) {
      imagefilter($img, IMG_FILTER_NEGATE);
    }
    
    // ---
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, self::IMG_WIDTH_MAX, self::IMG_HEIGHT ];
  }
  
  public function generateTwisterHSimplex($char_count) {
    if (!$this->font_id) {
      return false;
    }
    
    $challenge_str = $this->getRandomString($char_count);
    
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    
    // Foreground fog
    $img = $this->generateSimplexBg();
    
    $bleach = imagecolorallocatealpha($img, 255, 255, 255, mt_rand(80, 100));
    $grid_size = mt_rand(floor($img_height * 0.35), floor($img_height * 0.55));
    $this->addGridLines($img, $img_width, $img_height, $bleach, $grid_size, 2);
    $this->addPowder($img, $img_width, $img_height, 0.08, $bleach);
    
    // Background fog
    $img_bg = $this->generateFogBg();
    
    // Text image
    list($img_chars_orig) = $this->generateImage($challenge_str, $char_count);
    $img_chars = $this->cloneTextImage($img_chars_orig);
    
    $this->removeBackground($img_chars);
    
    // Copy the text to the main fog image
    $this->applyTextToSimplexBg($img_chars, $img, 2, -2);
    
    // Prepare the text for the bg image
    $bg_chars = imagecreatetruecolor($img_width, $img_height);
    $_c = imagecolorallocatealpha($img, 0, 0, 0, 127);
    imagealphablending($bg_chars, false);
    imagefilledrectangle($bg_chars, 0, 0, $img_width, $img_height, $_c);
    $this->applyTextToSimplexBg($img_chars, $bg_chars, 2, -2);
    
    if ($this->useOverlayId) {
      $this->overlayPattern($img, $this->useOverlayId, $this->useOverlayDark);
    }
    
    if ($this->useInvert) {
      imagefilter($img, IMG_FILTER_NEGATE);
    }
    
    if ($this->useScoreLines) {
      $this->addScoreLines($img, $img_width, $img_height, 1);
    }
    
    if ($this->useGridLines) {
      $this->addGridLines($img, $img_width, $img_height, 1);
    }
    
    // Slice
    $slider_range = $this->sliceVerticalSimplex($img, $img_bg, $bg_chars);
    
    // ---
    
    imagetruecolortopalette($img, false, 50);
    
    return [ $challenge_str, $img, $img_width, $img_height, $img_bg, $img_width + $slider_range, 0 ];
  }
  
  private function cloneTextImage($img) {
    $img_width = self::IMG_WIDTH_MAX;
    $img_height = self::IMG_HEIGHT;
    $tmp = imagecreatetruecolor($img_width, $img_height);
    imagealphablending($tmp, false);
    imagecopy($tmp, $img, 0, 0, 0, 0, $img_width, $img_height);
    imagealphablending($tmp, true);
    return $tmp;
  }
  
  public static function getChallengeHash($solution, $params) {
    $uniq_id = self::b64_encode_url(openssl_random_pseudo_bytes(self::CHALLENGE_ID_BYTES));
    
    if (!$uniq_id) {
      return false;
    }
    
    if (!is_array($params) || empty($params)) {
      return false;
    }
    
    $data = "$uniq_id $solution " . implode(' ', $params);
    
    $challenge_hash = hash_hmac('sha256', $data, base64_decode(self::$hmac_secret));
    
    if (!$challenge_hash) {
      return false;
    }
    
    return [ $uniq_id, $challenge_hash ];
  }
  
  public static function verifyChallengeHash($challenge_hash, $uniq_id, $solution, $params) {
    if (!$challenge_hash || !$uniq_id || !$solution) {
      return false;
    }
    
    if (!is_array($params) || empty($params)) {
      return false;
    }
    
    $data = "$uniq_id $solution " . implode(' ', $params);
    
    $this_hash = hash_hmac('sha256', $data, base64_decode(self::$hmac_secret));
    
    if (!$this_hash) {
      return false;
    }
    
    return $this_hash === $challenge_hash;
  }
  
  public static function getBase64Images($img, $img_bg = null) {
    ob_start();
    imagepng($img);
    $url_img = base64_encode(ob_get_contents());
    
    if ($img_bg) {
      ob_clean();
      imagepng($img_bg);
      $url_img_bg = base64_encode(ob_get_contents());
    }
    else {
      $url_img_bg = null;
    }
    
    ob_end_clean();
    
    return [ $url_img, $url_img_bg ];
  }
  
  public static function normalizeReponseStr($str) {
    return str_replace(
      ['F', 'B', 'O', 'Z', 'U', '5', '6', 'Q'],
      ['P', '8', '0', '2', 'V', 'S', 'G', '0'],
      strtoupper(preg_replace('/[^a-zA-Z0-9]+/', '', $str))
    );
  }
  
  private static function b64_encode_url($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
  }
}

class SimplexNoise {
  const G2 = (3.0 - M_SQRT3) / 6.0;
  
  const Grad = [
    [1, 1],
    [-1, 1],
    [1, -1],
    [-1, -1],
    [1, 0],
    [-1, 0],
    [1, 0],
    [-1, 0],
    [0, 1],
    [0, -1],
    [0, 1],
    [0, -1],
  ];
  
  private $perm = [];
  private $permMod12 = [];
  
  public function __construct() {
    $max_rand = mt_getrandmax();
    
    $p = [];
    
    for ($i = 0; $i < 256; $i++) {
      $p[$i] = $i;
    }
    
    for ($i = 255; $i > 0; $i--) {
      $n = floor(($i + 1) * (mt_rand() / $max_rand));
      $q = $p[$i];
      $p[$i] = $p[$n];
      $p[$n] = $q;
    }
    
    for ($i = 0; $i < 512; $i++) {
      $this->perm[$i] = $p[$i & 255];
      $this->permMod12[$i] = $this->perm[$i] % 12;
    }
  }
  
  public function noise($x, $y) {
    $s = ($x + $y) * 0.5 * (M_SQRT3 - 1.0);
    $i = floor($x + $s);
    $j = floor($y + $s);
    $t = ($i + $j) * self::G2;
    $X0 = $i - $t;
    $Y0 = $j - $t;
    $x0 = $x - $X0;
    $y0 = $y - $Y0;

    $i1 = $x0 > $y0 ? 1 : 0;
    $j1 = $x0 > $y0 ? 0 : 1;

    $xG = $x0 + self::G2;
    $yG = $y0 + self::G2;
    $x1 = $xG - $i1;
    $y1 = $yG - $j1;
    $x2 = $xG + self::G2 - 1.0;
    $y2 = $yG + self::G2 - 1.0;
    
    $ii = $i & 255;
    $jj = $j & 255;
    $g0 = self::Grad[$this->permMod12[$ii + $this->perm[$jj]]];
    $g1 = self::Grad[$this->permMod12[$ii + $i1 + $this->perm[$jj + $j1]]];
    $g2 = self::Grad[$this->permMod12[$ii + 1 + $this->perm[$jj + 1]]];

    $t0 = 0.5 - $x0 * $x0 - $y0 * $y0;
    $n0 = $t0 < 0 ? 0.0 : ($t0 * $t0 * $t0 * $t0) * ($g0[0] * $x0 + $g0[1] * $y0);

    $t1 = 0.5 - $x1 * $x1 - $y1 * $y1;
    $n1 = $t1 < 0 ? 0.0 : ($t1 * $t1 * $t1 * $t1) * ($g1[0] * $x1 + $g1[1] * $y1);

    $t2 = 0.5 - $x2 * $x2 - $y2 * $y2;
    $n2 = $t2 < 0 ? 0.0 : ($t2 * $t2 * $t2 * $t2) * ($g2[0] * $x2 + $g2[1] * $y2);

    return 70.14805770653952 * ($n0 + $n1 + $n2);
  }
}
