From c6c8ef9c1e925312a1ff3f2a44b0cd5d3c72cd6a Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 07:42:59 +1000 Subject: [PATCH 1/9] add friendsofphp/php-cs-fixer --- .gitignore | 1 + .php-cs-fixer.php | 75 ++++++++++++++++++++++++++ composer.json | 1 + src/Gregwar/Captcha/CaptchaBuilder.php | 2 +- src/Gregwar/Captcha/PhraseBuilder.php | 2 +- 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 .php-cs-fixer.php diff --git a/.gitignore b/.gitignore index f40d3d1..51c3a35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea/ +/.php-cs-fixer.cache /.phpunit.result.cache /composer.lock /demo/*.jpg diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..09f77c8 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,75 @@ +in('src/') + ->in('tests/') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'simplified_if_return' => true, + 'no_superfluous_phpdoc_tags' => true, + 'single_class_element_per_statement' => true, + 'class_attributes_separation' => ['elements' => ['method' => 'one']], + 'no_superfluous_phpdoc_tags' => true, + 'single_class_element_per_statement' => true, + 'array_syntax' => [ + 'syntax' => 'short' + ], + 'blank_line_after_namespace' => true, + 'blank_line_before_statement' => [ + 'statements' => ['declare', 'return'] + ], + 'blank_line_between_import_groups' => true, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'constant_case' => true, + 'braces_position' => [ + 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', + 'allow_single_line_empty_anonymous_classes' => true, + 'allow_single_line_anonymous_functions' => true + ], + 'declare_equal_normalize' => [ + 'space' => 'none' + ], + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => false, + ], + 'trailing_comma_in_multiline' => [ + 'elements' => ['parameters'], + ], + 'multiline_comment_opening_closing' => true, + 'no_break_comment' => false, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_whitespace_in_blank_line' => true, + 'return_type_declaration' => [ + 'space_before' => 'none', + ], + 'single_blank_line_at_eof' => true, + 'strict_param' => false, + 'modifier_keywords' => [ + 'elements' => ['const', 'method', 'property'] + ], + 'no_unused_imports' => true, + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha' + ] + ]) + ->setIndent(" ") + ->setFinder($finder) + ->setLineEnding("\n") + ->setUsingCache(true) + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) +; + diff --git a/composer.json b/composer.json index b644f28..807c5af 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ } }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.49", "phpunit/phpunit": "^9.6", "phpstan/phpstan": "2.1.17", "squizlabs/php_codesniffer": "4.0.1", diff --git a/src/Gregwar/Captcha/CaptchaBuilder.php b/src/Gregwar/Captcha/CaptchaBuilder.php index 4337645..132d5c6 100644 --- a/src/Gregwar/Captcha/CaptchaBuilder.php +++ b/src/Gregwar/Captcha/CaptchaBuilder.php @@ -473,7 +473,7 @@ public function buildAgainstOCR( int $width = 150, int $height = 40, ?string $font = null, - ?array $fingerprint = null + ?array $fingerprint = null, ): void { do { $this->build($width, $height, $font, $fingerprint); diff --git a/src/Gregwar/Captcha/PhraseBuilder.php b/src/Gregwar/Captcha/PhraseBuilder.php index 8a4135e..16636bf 100644 --- a/src/Gregwar/Captcha/PhraseBuilder.php +++ b/src/Gregwar/Captcha/PhraseBuilder.php @@ -15,7 +15,7 @@ class PhraseBuilder implements PhraseBuilderInterface { public function __construct( public int $length = 5, - public string $charset = 'abcdefghijklmnpqrstuvwxyz123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + public string $charset = 'abcdefghijklmnpqrstuvwxyz123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', ) { // } From c3e6b83d738817a40730ac1968cd83834ddbc198 Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 07:50:09 +1000 Subject: [PATCH 2/9] Add ordered_class_elements to cs-fixer --- .php-cs-fixer.php | 21 + src/Gregwar/Captcha/CaptchaBuilder.php | 976 +++++++++--------- .../Captcha/CaptchaBuilderInterface.php | 10 +- src/Gregwar/Captcha/PhraseBuilder.php | 12 +- tests/Captcha/CaptchaBuilderTest.php | 38 +- 5 files changed, 539 insertions(+), 518 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 09f77c8..8f4aefc 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -64,6 +64,27 @@ 'no_unused_imports' => true, 'ordered_imports' => [ 'sort_algorithm' => 'alpha' + ], + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'case', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public_static', + 'property_protected_static', + 'property_private_static', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'method_public', + 'method_protected', + 'method_private', + 'magic', + ], + 'sort_algorithm' => 'alpha', ] ]) ->setIndent(" ") diff --git a/src/Gregwar/Captcha/CaptchaBuilder.php b/src/Gregwar/Captcha/CaptchaBuilder.php index 132d5c6..fb6d03f 100644 --- a/src/Gregwar/Captcha/CaptchaBuilder.php +++ b/src/Gregwar/Captcha/CaptchaBuilder.php @@ -25,45 +25,45 @@ class CaptchaBuilder implements CaptchaBuilderInterface /** Temporary dir, for OCR check */ public string $tempDir = ''; - /** @var int[] $fingerprint */ - protected array $fingerprint = []; - - protected bool $useFingerprint = false; - - /** @var array>|null $textColor */ - protected ?array $textColor = null; - - /** @var array>|null $lineColor */ - protected ?array $lineColor = null; + /** + * Allowed image types for the background images + * + * @var string[] + */ + protected array $allowedBackgroundImageTypes = ['image/png', 'image/jpeg', 'image/gif']; /** @var array>|null $backgroundColor */ protected ?array $backgroundColor = null; - /** @var int<0, 127> $bgAlpha */ - protected int $bgAlpha = 0; - /** @var string[] $backgroundImages */ protected array $backgroundImages = []; - protected ?GdImage $contents = null; - - protected ?string $phrase = null; + /** @var int<0, 127> $bgAlpha */ + protected int $bgAlpha = 0; protected ?PhraseBuilderInterface $builder = null; + protected ?GdImage $contents = null; + protected bool $distortion = true; + /** @var int[] $fingerprint */ + protected array $fingerprint = []; + /** - * The maximum number of lines to draw in front of - * the image. null - use default algorithm + * Ignore all effects */ - protected ?int $maxFrontLines = null; + protected bool $ignoreAllEffects = false; + + protected string $imageType = "jpeg"; /** - * The maximum number of lines to draw behind - * the image. null - use default algorithm + * Is the interpolation enabled? */ - protected ?int $maxBehindLines = null; + protected bool $interpolation = true; + + /** @var array>|null $lineColor */ + protected ?array $lineColor = null; /** * The maximum angle of char @@ -71,30 +71,30 @@ class CaptchaBuilder implements CaptchaBuilderInterface protected int $maxAngle = 8; /** - * The maximum offset of char + * The maximum number of lines to draw behind + * the image. null - use default algorithm */ - protected int $maxOffset = 5; + protected ?int $maxBehindLines = null; /** - * Is the interpolation enabled? + * The maximum number of lines to draw in front of + * the image. null - use default algorithm */ - protected bool $interpolation = true; + protected ?int $maxFrontLines = null; /** - * Ignore all effects + * The maximum offset of char */ - protected bool $ignoreAllEffects = false; + protected int $maxOffset = 5; + + protected ?string $phrase = null; protected bool $scatterEffect = true; - /** - * Allowed image types for the background images - * - * @var string[] - */ - protected array $allowedBackgroundImageTypes = ['image/png', 'image/jpeg', 'image/gif']; + /** @var array>|null $textColor */ + protected ?array $textColor = null; - protected string $imageType = "jpeg"; + protected bool $useFingerprint = false; public function __construct(?string $phrase = null, ?PhraseBuilderInterface $builder = null) { @@ -106,111 +106,120 @@ public function __construct(?string $phrase = null, ?PhraseBuilderInterface $bui } /** - * The image contents - */ - public function getContents(): ?GdImage - { - return $this->contents; - } - - /** - * Enable/Disables the interpolation + * Generate the image + * @param int[] $fingerprint + * @throws CaptchaException */ - public function setInterpolation(bool $interpolate = true): static - { - $this->interpolation = $interpolate; - - return $this; - } - - public function setImageType(?string $imageType = null): static + public function build(int $width = 150, int $height = 40, ?string $font = null, ?array $fingerprint = null): static { - $imageType = is_string($imageType) ? strtolower($imageType) : ''; - if (in_array($imageType, ['png', 'jpeg', 'gif'])) { - $this->imageType = $imageType; + if (null !== $fingerprint) { + $this->fingerprint = $fingerprint; + $this->useFingerprint = true; + } else { + $this->fingerprint = []; + $this->useFingerprint = false; } - return $this; - } + if ($font === null) { + $font = __DIR__ . '/Font/captcha' . $this->rand(0, 5) . '.ttf'; + } - public function getImageType(): string - { - return $this->imageType; - } + $bg = 0; + if (empty($this->backgroundImages) && $width > 0 && $height > 0) { + // if background images list is not set, use a color fill as a background + $image = imagecreatetruecolor($width, $height); + if ($this->backgroundColor == null) { + $bg = imagecolorallocatealpha( + $image, + $this->rand(200, 255), + $this->rand(200, 255), + $this->rand(200, 255), + $this->bgAlpha + ); + } else { + $color = $this->backgroundColor; + $bg = imagecolorallocatealpha($image, $color[0], $color[1], $color[2], $this->bgAlpha); + } - /** - * Setting the phrase - */ - public function setPhrase(?string $phrase = null): void - { - $this->phrase = $phrase; - } + imagefill($image, 0, 0, $bg ?: 0); + imagesavealpha($image, true); + } else { + // use a random background image + $randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages) - 1)]; - /** - * Enables/disable distortion - */ - public function setDistortion(bool|int $distortion): static - { - $this->distortion = (bool) $distortion; + $imageType = $this->validateBackgroundImage($randomBackgroundImage); - return $this; - } + $image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType); + } + if (!$image) { + throw new LogicException('Failed to create background image'); + } + // Apply effects + if (!$this->ignoreAllEffects) { + $square = $width * $height; + $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); - /** - * Enables/disable scatter effect - Only applies to PHP 7.4+ - */ - public function setScatterEffect(bool $scatterEffect): static - { - $this->scatterEffect = $scatterEffect; + // set the maximum number of lines to draw in front of the text + if ($this->maxBehindLines != null && $this->maxBehindLines > 0) { + $effects = min($this->maxBehindLines, $effects); + } - return $this; - } + if ($this->maxBehindLines !== 0) { + for ($e = 0; $e < $effects; $e++) { + $this->drawLine($image, $width, $height); + } + } + } - public function setMaxBehindLines(?int $maxBehindLines): static - { - $this->maxBehindLines = $maxBehindLines; + // Write CAPTCHA text + $color = $this->writePhrase($image, $this->phrase, $font, $width, $height); - return $this; - } + // Apply effects + if (!$this->ignoreAllEffects) { + $square = $width * $height; + $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); - public function setMaxFrontLines(?int $maxFrontLines): static - { - $this->maxFrontLines = $maxFrontLines; + // set the maximum number of lines to draw in front of the text + if ($this->maxFrontLines != null && $this->maxFrontLines > 0) { + $effects = min($this->maxFrontLines, $effects); + } - return $this; - } + if ($this->maxFrontLines !== 0) { + for ($e = 0; $e < $effects; $e++) { + $this->drawLine($image, $width, $height, $color); + } + } + } - public function setMaxAngle(int $maxAngle): static - { - $this->maxAngle = $maxAngle; + // Distort the image + if ($this->distortion && !$this->ignoreAllEffects && $width > 0 && $height > 0) { + $image = $this->distort($image, $width, $height, $bg ?: 0); + } - return $this; - } + // Post effects + if (!$this->ignoreAllEffects && $image) { + $this->postEffect($image, $bg ?: 0); + } - public function setMaxOffset(int $maxOffset): static - { - $this->maxOffset = $maxOffset; + $this->contents = $image; return $this; } /** - * Gets the captcha phrase - */ - public function getPhrase(): ?string - { - return $this->phrase; - } - - /** - * Returns true if the given phrase is good + * Builds while the code is readable against an OCR + * @param int[] $fingerprint + * @throws CaptchaException */ - public function testPhrase(string $phrase): bool - { - return ( - $this->getPhrase() - && $this->builder - && $this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase())); + public function buildAgainstOCR( + int $width = 150, + int $height = 40, + ?string $font = null, + ?array $fingerprint = null, + ): void { + do { + $this->build($width, $height, $font, $fingerprint); + } while ($this->isOCRReadable()); } /** @@ -222,50 +231,70 @@ public static function create(?string $phrase = null): CaptchaBuilder } /** - * Sets the text color to use - * @param int<0, 255> $r - * @param int<0, 255> $g - * @param int<0, 255> $b + * Distorts the image + * @param int<1, max> $width + * @param int<1, max> $height */ - public function setTextColor(int $r, int $g, int $b): static + public function distort(GdImage $image, int $width, int $height, int $bg = 0): ?GdImage { - $this->textColor = [$r, $g, $b]; - - return $this; - } + $contents = imagecreatetruecolor($width, $height); + imagefill($contents, 0, 0, $bg); + imagesavealpha($contents, true); + $X = $this->rand(0, $width); + $Y = $this->rand(0, $height); + $phase = $this->rand(0, 10); + $scale = 1.1 + $this->rand(0, 10000) / 30000; + for ($x = 0; $x < $width; $x++) { + for ($y = 0; $y < $height; $y++) { + $Vx = $x - $X; + $Vy = $y - $Y; + $Vn = sqrt($Vx * $Vx + $Vy * $Vy); - /** - * Sets the background color to use - * @param int<0, 255> $r - * @param int<0, 255> $g - * @param int<0, 255> $b - */ - public function setBackgroundColor(int $r, int $g, int $b): static - { - $this->backgroundColor = [$r, $g, $b]; + if ($Vn != 0) { + $Vn2 = $Vn + 4 * sin($Vn / 30); + $nX = $X + ($Vx * $Vn2 / $Vn); + $nY = $Y + ($Vy * $Vn2 / $Vn); + } else { + $nX = $X; + $nY = $Y; + } + $nY = $nY + $scale * sin($phase + $nX * 0.2); - return $this; + if ($this->interpolation) { + $p = $this->interpolate( + $nX - floor($nX), + $nY - floor($nY), + $this->getCol($image, floor($nX), floor($nY), $bg), + $this->getCol($image, ceil($nX), floor($nY), $bg), + $this->getCol($image, floor($nX), ceil($nY), $bg), + $this->getCol($image, ceil($nX), ceil($nY), $bg) + ); + } else { + $p = $this->getCol($image, round($nX), round($nY), $bg); + } + + if ($p == 0) { + $p = $bg; + } + + imagesetpixel($contents, $x, $y, $p); + } + } + + return $contents ?: null; } /** - * @param int<0, 127> $alpha 0 to 127, 127 is completely transparent - * @return $this - * @throws InvalidArgumentException - * @throws LogicException + * Gets the image contents + * + * @throws CaptchaException */ - public function setBackgroundAlpha(int $alpha): static + public function get(int $quality = 90): string { - if ($alpha < 0 || $alpha > 127) { - throw new InvalidArgumentException('Argument $alpha must be an integer between 0 and 127.'); - } - - if ($this->getImageType() !== 'png') { - throw new LogicException('You can only set transparency on PNG images, call setImageType(\'png\')'); - } - - $this->bgAlpha = $alpha; + ob_start(); + $this->output($quality); - return $this; + return ob_get_clean() ?: ''; } public function getBackgroundAlpha(): int @@ -274,158 +303,48 @@ public function getBackgroundAlpha(): int } /** - * Sets the line color to use - * @param int<0, 255> $r - * @param int<0, 255> $g - * @param int<0, 255> $b + * The image contents */ - public function setLineColor(int $r, int $g, int $b): static + public function getContents(): ?GdImage { - $this->lineColor = [$r, $g, $b]; - - return $this; + return $this->contents; } /** - * Sets the ignoreAllEffects value + * @return int[] */ - public function setIgnoreAllEffects(bool $ignoreAllEffects): static + public function getFingerprint(): array { - $this->ignoreAllEffects = $ignoreAllEffects; - - return $this; + return $this->fingerprint; } /** - * Sets the list of background images to use (one image is randomly selected) - * @param string[] $backgroundImages + * Gets the image GD */ - public function setBackgroundImages(array $backgroundImages): static + public function getGd(): ?GdImage { - $this->backgroundImages = $backgroundImages; - - return $this; + return $this->contents; } - /** - * Draw lines over the image - */ - protected function drawLine(GdImage $image, int $width = 150, int $height = 40, ?int $tcol = null): void + public function getImageType(): string { - if ($this->lineColor !== null) { - $red = $this->lineColor[0]; - $green = $this->lineColor[1]; - $blue = $this->lineColor[2]; - } else { - $red = $this->rand(100, 255); - $green = $this->rand(100, 255); - $blue = $this->rand(100, 255); - } - - if ($tcol === null) { - $tcol = imagecolorallocate($image, $red, $green, $blue); - } - - if ($this->rand(0, 1)) { // Horizontal - $Xa = $this->rand(0, (int)($width / 2)); - $Ya = $this->rand(0, $height); - $Xb = $this->rand((int)($width / 2), $width); - $Yb = $this->rand(0, $height); - } else { // Vertical - $Xa = $this->rand(0, $width); - $Ya = $this->rand(0, (int)($height / 2)); - $Xb = $this->rand(0, $width); - $Yb = $this->rand((int)($height / 2), $height); - } - imagesetthickness($image, $this->rand(1, 3)); - imageline($image, $Xa, $Ya, $Xb, $Yb, (int)$tcol); + return $this->imageType; } /** - * Apply some post effects + * Gets the captcha phrase */ - protected function postEffect(GdImage $image, ?int $bg = null): void + public function getPhrase(): ?string { - if (!function_exists('imagefilter')) { - return; - } - - if ($this->backgroundColor != null || $this->textColor != null) { - return; - } - - // Scatter/Noise - Added in PHP 7.4 - $scattered = false; - if (defined('IMG_FILTER_SCATTER')) { - if ($this->scatterEffect && $this->rand(0, 3) != 0 && $bg != null) { - $scattered = true; - imagefilter($image, IMG_FILTER_SCATTER, 0, 2, [$bg]); - } - } - - // Negate ? - if ($this->rand(0, 1) == 0) { - imagefilter($image, IMG_FILTER_NEGATE); - } - - // Edge ? - if (!$scattered && $this->rand(0, 10) == 0) { - imagefilter($image, IMG_FILTER_EDGEDETECT); - } - - // Contrast - imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10)); - - // Colorize - if (!$scattered && $this->rand(0, 5) == 0) { - imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50)); - } + return $this->phrase; } /** - * Writes the phrase on the image + * Gets the HTML inline base64 */ - protected function writePhrase(GdImage $image, ?string $phrase, string $font, int $width, int $height): ?int + public function inline(int $quality = 90): string { - $length = mb_strlen((string)$phrase); - if ($length === 0 || !$phrase) { - return imagecolorallocate($image, 0, 0, 0) ?: null; - } - - // Gets the text size and start position - $size = (int) round($width / $length) - $this->rand(0, 3) - 1; - $box = imagettfbbox($size, 0, $font, $phrase); - if (!$box) { - return null; - } - $textWidth = $box[2] - $box[0]; - $textHeight = $box[1] - $box[7]; - $x = (int) round(($width - $textWidth) / 2); - $y = (int) round(($height - $textHeight) / 2) + $size; - - if (!$this->textColor) { - $textColor = [$this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)]; - } else { - $textColor = $this->textColor; - } - $col = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); - if ($col !== false) { - // Write the letters one by one, with random angle - for ($i = 0; $i < $length; $i++) { - $symbol = mb_substr($phrase, $i, 1); - $box = imagettfbbox($size, 0, $font, $symbol); - if (!$box) { - return null; - } - $w = $box[2] - $box[0]; - $angle = $this->rand(-$this->maxAngle, $this->maxAngle); - $offset = $this->rand(-$this->maxOffset, $this->maxOffset); - imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol); - $x += $w; - } - } - - return $col ?: null; + return sprintf('data:image/%s;base64,%s', $this->getImageType(), base64_encode($this->get($quality))); } /** @@ -465,265 +384,280 @@ public function isOCRReadable(): bool } /** - * Builds while the code is readable against an OCR - * @param int[] $fingerprint + * Outputs the image * @throws CaptchaException */ - public function buildAgainstOCR( - int $width = 150, - int $height = 40, - ?string $font = null, - ?array $fingerprint = null, - ): void { - do { - $this->build($width, $height, $font, $fingerprint); - } while ($this->isOCRReadable()); + public function output(int $quality = 90): void + { + $this->save(null, $quality); } /** - * Generate the image - * @param int[] $fingerprint + * Saves the Captcha to file * @throws CaptchaException */ - public function build(int $width = 150, int $height = 40, ?string $font = null, ?array $fingerprint = null): static + public function save(?string $filename = null, int $quality = 90): void { - if (null !== $fingerprint) { - $this->fingerprint = $fingerprint; - $this->useFingerprint = true; - } else { - $this->fingerprint = []; - $this->useFingerprint = false; + $imageType = $this->getImageType(); + if (!$this->contents) { + throw new CaptchaException('No image generated'); + } + switch ($imageType) { + case "png": + imagepng($this->contents, $filename, (int)($quality / 10)); // quality 0-9 + break; + case "gif": + imagegif($this->contents, $filename); + break; + default: + imagejpeg($this->contents, $filename, $quality); // quality 0-100 + break; } + } - if ($font === null) { - $font = __DIR__ . '/Font/captcha' . $this->rand(0, 5) . '.ttf'; + /** + * @param int<0, 127> $alpha 0 to 127, 127 is completely transparent + * @return $this + * @throws InvalidArgumentException + * @throws LogicException + */ + public function setBackgroundAlpha(int $alpha): static + { + if ($alpha < 0 || $alpha > 127) { + throw new InvalidArgumentException('Argument $alpha must be an integer between 0 and 127.'); } - $bg = 0; - if (empty($this->backgroundImages) && $width > 0 && $height > 0) { - // if background images list is not set, use a color fill as a background - $image = imagecreatetruecolor($width, $height); - if ($this->backgroundColor == null) { - $bg = imagecolorallocatealpha( - $image, - $this->rand(200, 255), - $this->rand(200, 255), - $this->rand(200, 255), - $this->bgAlpha - ); - } else { - $color = $this->backgroundColor; - $bg = imagecolorallocatealpha($image, $color[0], $color[1], $color[2], $this->bgAlpha); - } + if ($this->getImageType() !== 'png') { + throw new LogicException('You can only set transparency on PNG images, call setImageType(\'png\')'); + } - imagefill($image, 0, 0, $bg ?: 0); - imagesavealpha($image, true); - } else { - // use a random background image - $randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages) - 1)]; + $this->bgAlpha = $alpha; - $imageType = $this->validateBackgroundImage($randomBackgroundImage); + return $this; + } - $image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType); - } - if (!$image) { - throw new LogicException('Failed to create background image'); - } - // Apply effects - if (!$this->ignoreAllEffects) { - $square = $width * $height; - $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); + /** + * Sets the background color to use + * @param int<0, 255> $r + * @param int<0, 255> $g + * @param int<0, 255> $b + */ + public function setBackgroundColor(int $r, int $g, int $b): static + { + $this->backgroundColor = [$r, $g, $b]; - // set the maximum number of lines to draw in front of the text - if ($this->maxBehindLines != null && $this->maxBehindLines > 0) { - $effects = min($this->maxBehindLines, $effects); - } + return $this; + } - if ($this->maxBehindLines !== 0) { - for ($e = 0; $e < $effects; $e++) { - $this->drawLine($image, $width, $height); - } - } - } + /** + * Sets the list of background images to use (one image is randomly selected) + * @param string[] $backgroundImages + */ + public function setBackgroundImages(array $backgroundImages): static + { + $this->backgroundImages = $backgroundImages; - // Write CAPTCHA text - $color = $this->writePhrase($image, $this->phrase, $font, $width, $height); + return $this; + } - // Apply effects - if (!$this->ignoreAllEffects) { - $square = $width * $height; - $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); + /** + * Enables/disable distortion + */ + public function setDistortion(bool|int $distortion): static + { + $this->distortion = (bool) $distortion; - // set the maximum number of lines to draw in front of the text - if ($this->maxFrontLines != null && $this->maxFrontLines > 0) { - $effects = min($this->maxFrontLines, $effects); - } + return $this; + } - if ($this->maxFrontLines !== 0) { - for ($e = 0; $e < $effects; $e++) { - $this->drawLine($image, $width, $height, $color); - } - } - } + /** + * Sets the ignoreAllEffects value + */ + public function setIgnoreAllEffects(bool $ignoreAllEffects): static + { + $this->ignoreAllEffects = $ignoreAllEffects; - // Distort the image - if ($this->distortion && !$this->ignoreAllEffects && $width > 0 && $height > 0) { - $image = $this->distort($image, $width, $height, $bg ?: 0); - } + return $this; + } - // Post effects - if (!$this->ignoreAllEffects && $image) { - $this->postEffect($image, $bg ?: 0); + public function setImageType(?string $imageType = null): static + { + $imageType = is_string($imageType) ? strtolower($imageType) : ''; + if (in_array($imageType, ['png', 'jpeg', 'gif'])) { + $this->imageType = $imageType; } - $this->contents = $image; + return $this; + } + + /** + * Enable/Disables the interpolation + */ + public function setInterpolation(bool $interpolate = true): static + { + $this->interpolation = $interpolate; return $this; } /** - * Distorts the image - * @param int<1, max> $width - * @param int<1, max> $height + * Sets the line color to use + * @param int<0, 255> $r + * @param int<0, 255> $g + * @param int<0, 255> $b */ - public function distort(GdImage $image, int $width, int $height, int $bg = 0): ?GdImage + public function setLineColor(int $r, int $g, int $b): static { - $contents = imagecreatetruecolor($width, $height); - imagefill($contents, 0, 0, $bg); - imagesavealpha($contents, true); - $X = $this->rand(0, $width); - $Y = $this->rand(0, $height); - $phase = $this->rand(0, 10); - $scale = 1.1 + $this->rand(0, 10000) / 30000; - for ($x = 0; $x < $width; $x++) { - for ($y = 0; $y < $height; $y++) { - $Vx = $x - $X; - $Vy = $y - $Y; - $Vn = sqrt($Vx * $Vx + $Vy * $Vy); + $this->lineColor = [$r, $g, $b]; - if ($Vn != 0) { - $Vn2 = $Vn + 4 * sin($Vn / 30); - $nX = $X + ($Vx * $Vn2 / $Vn); - $nY = $Y + ($Vy * $Vn2 / $Vn); - } else { - $nX = $X; - $nY = $Y; - } - $nY = $nY + $scale * sin($phase + $nX * 0.2); + return $this; + } - if ($this->interpolation) { - $p = $this->interpolate( - $nX - floor($nX), - $nY - floor($nY), - $this->getCol($image, floor($nX), floor($nY), $bg), - $this->getCol($image, ceil($nX), floor($nY), $bg), - $this->getCol($image, floor($nX), ceil($nY), $bg), - $this->getCol($image, ceil($nX), ceil($nY), $bg) - ); - } else { - $p = $this->getCol($image, round($nX), round($nY), $bg); - } + public function setMaxAngle(int $maxAngle): static + { + $this->maxAngle = $maxAngle; - if ($p == 0) { - $p = $bg; - } + return $this; + } - imagesetpixel($contents, $x, $y, $p); - } - } + public function setMaxBehindLines(?int $maxBehindLines): static + { + $this->maxBehindLines = $maxBehindLines; + + return $this; + } + + public function setMaxFrontLines(?int $maxFrontLines): static + { + $this->maxFrontLines = $maxFrontLines; + + return $this; + } + + public function setMaxOffset(int $maxOffset): static + { + $this->maxOffset = $maxOffset; - return $contents ?: null; + return $this; } /** - * Saves the Captcha to file - * @throws CaptchaException + * Setting the phrase */ - public function save(?string $filename = null, int $quality = 90): void + public function setPhrase(?string $phrase = null): void { - $imageType = $this->getImageType(); - if (!$this->contents) { - throw new CaptchaException('No image generated'); - } - switch ($imageType) { - case "png": - imagepng($this->contents, $filename, (int)($quality / 10)); // quality 0-9 - break; - case "gif": - imagegif($this->contents, $filename); - break; - default: - imagejpeg($this->contents, $filename, $quality); // quality 0-100 - break; - } + $this->phrase = $phrase; } /** - * Gets the image GD + * Enables/disable scatter effect - Only applies to PHP 7.4+ */ - public function getGd(): ?GdImage + public function setScatterEffect(bool $scatterEffect): static { - return $this->contents; + $this->scatterEffect = $scatterEffect; + + return $this; } /** - * Gets the image contents - * - * @throws CaptchaException + * Sets the text color to use + * @param int<0, 255> $r + * @param int<0, 255> $g + * @param int<0, 255> $b */ - public function get(int $quality = 90): string + public function setTextColor(int $r, int $g, int $b): static { - ob_start(); - $this->output($quality); + $this->textColor = [$r, $g, $b]; - return ob_get_clean() ?: ''; + return $this; } /** - * Gets the HTML inline base64 + * Returns true if the given phrase is good */ - public function inline(int $quality = 90): string + public function testPhrase(string $phrase): bool { - return sprintf('data:image/%s;base64,%s', $this->getImageType(), base64_encode($this->get($quality))); + return ( + $this->getPhrase() + && $this->builder + && $this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase())); } /** - * Outputs the image + * Create background image from type * @throws CaptchaException */ - public function output(int $quality = 90): void + protected function createBackgroundImageFromType(string $backgroundImage, ?string $imageType = null): GdImage { - $this->save(null, $quality); + $image = match ($imageType) { + 'image/jpeg' => imagecreatefromjpeg($backgroundImage), + 'image/png' => imagecreatefrompng($backgroundImage), + 'image/gif' => imagecreatefromgif($backgroundImage), + default => throw new InvalidArgumentException('Not supported file type for background image!'), + }; + + if ($image === false) { + throw new LogicException('Failed to create background image!'); + } + + return $image; } /** - * @return int[] + * Draw lines over the image */ - public function getFingerprint(): array + protected function drawLine(GdImage $image, int $width = 150, int $height = 40, ?int $tcol = null): void { - return $this->fingerprint; + if ($this->lineColor !== null) { + $red = $this->lineColor[0]; + $green = $this->lineColor[1]; + $blue = $this->lineColor[2]; + } else { + $red = $this->rand(100, 255); + $green = $this->rand(100, 255); + $blue = $this->rand(100, 255); + } + + if ($tcol === null) { + $tcol = imagecolorallocate($image, $red, $green, $blue); + } + + if ($this->rand(0, 1)) { // Horizontal + $Xa = $this->rand(0, (int)($width / 2)); + $Ya = $this->rand(0, $height); + $Xb = $this->rand((int)($width / 2), $width); + $Yb = $this->rand(0, $height); + } else { // Vertical + $Xa = $this->rand(0, $width); + $Ya = $this->rand(0, (int)($height / 2)); + $Xb = $this->rand(0, $width); + $Yb = $this->rand((int)($height / 2), $height); + } + imagesetthickness($image, $this->rand(1, 3)); + imageline($image, $Xa, $Ya, $Xb, $Yb, (int)$tcol); } - /** - * Returns a random number or the next number in the - * fingerprint - * @return int<0, 255> - */ - protected function rand(int $min, int $max): int + protected function getCol(GdImage $image, float|int $x, float|int $y, int $background): int { - if ($this->useFingerprint) { - $value = (int)current($this->fingerprint); - $value = max(0, $value); - $value = min(255, $value); - next($this->fingerprint); - } else { - $value = mt_rand($min, $max); - $value = max(0, $value); - $value = min(255, $value); - $this->fingerprint[] = $value; + $L = imagesx($image); + $H = imagesy($image); + if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { + return $background; } - return $value; + return imagecolorat($image, (int)$x, (int)$y) ?: 0; + } + + /** + * @return array{int, int, int} + */ + protected function getRGB(int $col): array + { + return [ + ($col >> 16) & 0xff, + ($col >> 8) & 0xff, + $col & 0xff, + ]; } protected function interpolate(float $x, float $y, int $nw, int $ne, int $sw, int $se): int @@ -751,27 +685,67 @@ protected function interpolate(float $x, float $y, int $nw, int $ne, int $sw, in return ($r << 16) | ($g << 8) | $b; } - protected function getCol(GdImage $image, float|int $x, float|int $y, int $background): int + /** + * Apply some post effects + */ + protected function postEffect(GdImage $image, ?int $bg = null): void { - $L = imagesx($image); - $H = imagesy($image); - if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) { - return $background; + if (!function_exists('imagefilter')) { + return; } - return imagecolorat($image, (int)$x, (int)$y) ?: 0; + if ($this->backgroundColor != null || $this->textColor != null) { + return; + } + + // Scatter/Noise - Added in PHP 7.4 + $scattered = false; + if (defined('IMG_FILTER_SCATTER')) { + if ($this->scatterEffect && $this->rand(0, 3) != 0 && $bg != null) { + $scattered = true; + imagefilter($image, IMG_FILTER_SCATTER, 0, 2, [$bg]); + } + } + + // Negate ? + if ($this->rand(0, 1) == 0) { + imagefilter($image, IMG_FILTER_NEGATE); + } + + // Edge ? + if (!$scattered && $this->rand(0, 10) == 0) { + imagefilter($image, IMG_FILTER_EDGEDETECT); + } + + // Contrast + imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10)); + + // Colorize + if (!$scattered && $this->rand(0, 5) == 0) { + imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50)); + } } /** - * @return array{int, int, int} + * Returns a random number or the next number in the + * fingerprint + * @return int<0, 255> */ - protected function getRGB(int $col): array + protected function rand(int $min, int $max): int { - return [ - ($col >> 16) & 0xff, - ($col >> 8) & 0xff, - $col & 0xff, - ]; + if ($this->useFingerprint) { + $value = (int)current($this->fingerprint); + $value = max(0, $value); + $value = min(255, $value); + next($this->fingerprint); + } else { + $value = mt_rand($min, $max); + $value = max(0, $value); + $value = min(255, $value); + $this->fingerprint[] = $value; + } + + return $value; } /** @@ -810,22 +784,48 @@ protected function validateBackgroundImage(string $backgroundImage): ?string } /** - * Create background image from type - * @throws CaptchaException + * Writes the phrase on the image */ - protected function createBackgroundImageFromType(string $backgroundImage, ?string $imageType = null): GdImage + protected function writePhrase(GdImage $image, ?string $phrase, string $font, int $width, int $height): ?int { - $image = match ($imageType) { - 'image/jpeg' => imagecreatefromjpeg($backgroundImage), - 'image/png' => imagecreatefrompng($backgroundImage), - 'image/gif' => imagecreatefromgif($backgroundImage), - default => throw new InvalidArgumentException('Not supported file type for background image!'), - }; + $length = mb_strlen((string)$phrase); + if ($length === 0 || !$phrase) { + return imagecolorallocate($image, 0, 0, 0) ?: null; + } - if ($image === false) { - throw new LogicException('Failed to create background image!'); + // Gets the text size and start position + $size = (int) round($width / $length) - $this->rand(0, 3) - 1; + $box = imagettfbbox($size, 0, $font, $phrase); + if (!$box) { + return null; } + $textWidth = $box[2] - $box[0]; + $textHeight = $box[1] - $box[7]; + $x = (int) round(($width - $textWidth) / 2); + $y = (int) round(($height - $textHeight) / 2) + $size; - return $image; + if (!$this->textColor) { + $textColor = [$this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)]; + } else { + $textColor = $this->textColor; + } + $col = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]); + if ($col !== false) { + // Write the letters one by one, with random angle + for ($i = 0; $i < $length; $i++) { + $symbol = mb_substr($phrase, $i, 1); + $box = imagettfbbox($size, 0, $font, $symbol); + if (!$box) { + return null; + } + $w = $box[2] - $box[0]; + $angle = $this->rand(-$this->maxAngle, $this->maxAngle); + $offset = $this->rand(-$this->maxOffset, $this->maxOffset); + imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol); + $x += $w; + } + } + + return $col ?: null; } } diff --git a/src/Gregwar/Captcha/CaptchaBuilderInterface.php b/src/Gregwar/Captcha/CaptchaBuilderInterface.php index a7f4661..67a9c78 100644 --- a/src/Gregwar/Captcha/CaptchaBuilderInterface.php +++ b/src/Gregwar/Captcha/CaptchaBuilderInterface.php @@ -15,11 +15,6 @@ interface CaptchaBuilderInterface */ public function build(int $width = 150, int $height = 40, ?string $font = null, ?array $fingerprint = null): static; - /** - * Saves the code to a file - */ - public function save(?string $filename = null, int $quality = 90): void; - /** * Gets the image contents */ @@ -29,4 +24,9 @@ public function get(int $quality = 90): string; * Outputs the image */ public function output(int $quality = 90): void; + + /** + * Saves the code to a file + */ + public function save(?string $filename = null, int $quality = 90): void; } diff --git a/src/Gregwar/Captcha/PhraseBuilder.php b/src/Gregwar/Captcha/PhraseBuilder.php index 16636bf..0053ef8 100644 --- a/src/Gregwar/Captcha/PhraseBuilder.php +++ b/src/Gregwar/Captcha/PhraseBuilder.php @@ -42,11 +42,11 @@ public function build(?int $length = null, ?string $charset = null): string } /** - * "Niceize" a code + * A static helper to compare */ - public function niceize(string $str): string + public static function comparePhrases(string $str1, string $str2): bool { - return self::doNiceize($str); + return self::doNiceize($str1) === self::doNiceize($str2); } /** @@ -58,11 +58,11 @@ public static function doNiceize(string $str): string } /** - * A static helper to compare + * "Niceize" a code */ - public static function comparePhrases(string $str1, string $str2): bool + public function niceize(string $str): string { - return self::doNiceize($str1) === self::doNiceize($str2); + return self::doNiceize($str); } private function getRandomCharacter(): string diff --git a/tests/Captcha/CaptchaBuilderTest.php b/tests/Captcha/CaptchaBuilderTest.php index f6d8a59..6ead6dd 100644 --- a/tests/Captcha/CaptchaBuilderTest.php +++ b/tests/Captcha/CaptchaBuilderTest.php @@ -20,11 +20,6 @@ class CaptchaBuilderTest extends TestCase 'world' ]; - public function testCreate(): void - { - $this->assertInstanceOf('Gregwar\Captcha\CaptchaBuilder', CaptchaBuilder::create()); - } - public function testBuild(): void { $this->assertInstanceOf('Gregwar\Captcha\CaptchaBuilder', CaptchaBuilder::create()->build()); @@ -35,6 +30,11 @@ public function testBuild(): void } } + public function testCreate(): void + { + $this->assertInstanceOf('Gregwar\Captcha\CaptchaBuilder', CaptchaBuilder::create()); + } + public function testDemo(): void { $captcha = new CaptchaBuilder(); @@ -54,6 +54,20 @@ public function testFingerPrint(): void $this->assertTrue(is_int($int)); // @phpstan-ignore function.alreadyNarrowedType } + public function testImageTransparency(): void + { + foreach ([0 => false, 127 => true] as $alpha => $expected) { + $captcha = new CaptchaBuilder(); + $captcha->setImageType('png') + ->setBackgroundColor(0, 0, 0) + ->setBackgroundAlpha($alpha) + ->build() + ->save($filename = __DIR__ . '/../generated/out.png'); + + $this->assertTransparency($filename, $expected); + } + } + public function testImageType(): void { $types = [ @@ -77,20 +91,6 @@ public function testImageType(): void } } - public function testImageTransparency(): void - { - foreach ([0 => false, 127 => true] as $alpha => $expected) { - $captcha = new CaptchaBuilder(); - $captcha->setImageType('png') - ->setBackgroundColor(0, 0, 0) - ->setBackgroundAlpha($alpha) - ->build() - ->save($filename = __DIR__ . '/../generated/out.png'); - - $this->assertTransparency($filename, $expected); - } - } - private function assertTransparency(string $filename, bool $expected): void { $image = imagecreatefrompng($filename); From dc121429bab5b1b709719275174922be8a5de6c3 Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 07:50:23 +1000 Subject: [PATCH 3/9] normalize line endings --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitattributes b/.gitattributes index 37df89d..1b4675a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,7 @@ /phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /tests/ export-ignore + +* text=auto +*.neon eol=lf +*.php eol=lf \ No newline at end of file From 3d81ee247a219870f66bb54dde802999ec7b2412 Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 07:52:14 +1000 Subject: [PATCH 4/9] static ordering missing --- .php-cs-fixer.php | 3 +++ src/Gregwar/Captcha/CaptchaBuilder.php | 16 ++++++------- src/Gregwar/Captcha/PhraseBuilder.php | 32 +++++++++++++------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 8f4aefc..6cbd6cf 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -79,6 +79,9 @@ 'property_protected', 'property_private', 'construct', + 'method_public_static', + 'method_protected_static', + 'method_private_static', 'method_public', 'method_protected', 'method_private', diff --git a/src/Gregwar/Captcha/CaptchaBuilder.php b/src/Gregwar/Captcha/CaptchaBuilder.php index fb6d03f..8ca5eb9 100644 --- a/src/Gregwar/Captcha/CaptchaBuilder.php +++ b/src/Gregwar/Captcha/CaptchaBuilder.php @@ -105,6 +105,14 @@ public function __construct(?string $phrase = null, ?PhraseBuilderInterface $bui $this->tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'captcha' . DIRECTORY_SEPARATOR; } + /** + * Instantiation + */ + public static function create(?string $phrase = null): CaptchaBuilder + { + return new self($phrase); + } + /** * Generate the image * @param int[] $fingerprint @@ -222,14 +230,6 @@ public function buildAgainstOCR( } while ($this->isOCRReadable()); } - /** - * Instantiation - */ - public static function create(?string $phrase = null): CaptchaBuilder - { - return new self($phrase); - } - /** * Distorts the image * @param int<1, max> $width diff --git a/src/Gregwar/Captcha/PhraseBuilder.php b/src/Gregwar/Captcha/PhraseBuilder.php index 0053ef8..e61afb1 100644 --- a/src/Gregwar/Captcha/PhraseBuilder.php +++ b/src/Gregwar/Captcha/PhraseBuilder.php @@ -20,6 +20,22 @@ public function __construct( // } + /** + * A static helper to compare + */ + public static function comparePhrases(string $str1, string $str2): bool + { + return self::doNiceize($str1) === self::doNiceize($str2); + } + + /** + * A static helper to niceize + */ + public static function doNiceize(string $str): string + { + return strtr(strtolower($str), '01', 'ol'); + } + /** * Generates a random phrase of given length with given charset */ @@ -41,22 +57,6 @@ public function build(?int $length = null, ?string $charset = null): string return $phrase; } - /** - * A static helper to compare - */ - public static function comparePhrases(string $str1, string $str2): bool - { - return self::doNiceize($str1) === self::doNiceize($str2); - } - - /** - * A static helper to niceize - */ - public static function doNiceize(string $str): string - { - return strtr(strtolower($str), '01', 'ol'); - } - /** * "Niceize" a code */ From 1897e71c5a5539339d762fa312af38a69cc43f86 Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 07:53:45 +1000 Subject: [PATCH 5/9] Update .gitattributes --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 1b4675a..a972456 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,4 +8,4 @@ * text=auto *.neon eol=lf -*.php eol=lf \ No newline at end of file +*.php eol=lf From 1ddd4ed0167fa3fc4aa6b17bb3824b07702f876e Mon Sep 17 00:00:00 2001 From: lachlan Date: Sat, 20 Jun 2026 08:06:49 +1000 Subject: [PATCH 6/9] dupe rule names --- .php-cs-fixer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 6cbd6cf..d963266 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -14,8 +14,6 @@ 'no_superfluous_phpdoc_tags' => true, 'single_class_element_per_statement' => true, 'class_attributes_separation' => ['elements' => ['method' => 'one']], - 'no_superfluous_phpdoc_tags' => true, - 'single_class_element_per_statement' => true, 'array_syntax' => [ 'syntax' => 'short' ], From bbdf27627730fd3d5801b1dc44fa4662f0a03bca Mon Sep 17 00:00:00 2001 From: lachlan Date: Mon, 22 Jun 2026 14:29:50 +1000 Subject: [PATCH 7/9] Add @PER-CS3x0 --- .php-cs-fixer.php | 2 +- src/Gregwar/Captcha/CaptchaBuilder.php | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d963266..5450d85 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -8,6 +8,7 @@ return (new PhpCsFixer\Config()) ->setRules([ '@PSR12' => true, + '@PER-CS3x0' => true, 'no_useless_else' => true, 'no_useless_return' => true, 'simplified_if_return' => true, @@ -25,7 +26,6 @@ 'concat_space' => [ 'spacing' => 'one' ], - 'constant_case' => true, 'braces_position' => [ 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', 'allow_single_line_empty_anonymous_classes' => true, diff --git a/src/Gregwar/Captcha/CaptchaBuilder.php b/src/Gregwar/Captcha/CaptchaBuilder.php index 8ca5eb9..6a24309 100644 --- a/src/Gregwar/Captcha/CaptchaBuilder.php +++ b/src/Gregwar/Captcha/CaptchaBuilder.php @@ -165,7 +165,7 @@ public function build(int $width = 150, int $height = 40, ?string $font = null, // Apply effects if (!$this->ignoreAllEffects) { $square = $width * $height; - $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); + $effects = $this->rand((int) ($square / 3000), (int) ($square / 2000)); // set the maximum number of lines to draw in front of the text if ($this->maxBehindLines != null && $this->maxBehindLines > 0) { @@ -185,7 +185,7 @@ public function build(int $width = 150, int $height = 40, ?string $font = null, // Apply effects if (!$this->ignoreAllEffects) { $square = $width * $height; - $effects = $this->rand((int)($square / 3000), (int)($square / 2000)); + $effects = $this->rand((int) ($square / 3000), (int) ($square / 2000)); // set the maximum number of lines to draw in front of the text if ($this->maxFrontLines != null && $this->maxFrontLines > 0) { @@ -404,7 +404,7 @@ public function save(?string $filename = null, int $quality = 90): void } switch ($imageType) { case "png": - imagepng($this->contents, $filename, (int)($quality / 10)); // quality 0-9 + imagepng($this->contents, $filename, (int) ($quality / 10)); // quality 0-9 break; case "gif": imagegif($this->contents, $filename); @@ -623,18 +623,18 @@ protected function drawLine(GdImage $image, int $width = 150, int $height = 40, } if ($this->rand(0, 1)) { // Horizontal - $Xa = $this->rand(0, (int)($width / 2)); + $Xa = $this->rand(0, (int) ($width / 2)); $Ya = $this->rand(0, $height); - $Xb = $this->rand((int)($width / 2), $width); + $Xb = $this->rand((int) ($width / 2), $width); $Yb = $this->rand(0, $height); } else { // Vertical $Xa = $this->rand(0, $width); - $Ya = $this->rand(0, (int)($height / 2)); + $Ya = $this->rand(0, (int) ($height / 2)); $Xb = $this->rand(0, $width); - $Yb = $this->rand((int)($height / 2), $height); + $Yb = $this->rand((int) ($height / 2), $height); } imagesetthickness($image, $this->rand(1, 3)); - imageline($image, $Xa, $Ya, $Xb, $Yb, (int)$tcol); + imageline($image, $Xa, $Ya, $Xb, $Yb, (int) $tcol); } protected function getCol(GdImage $image, float|int $x, float|int $y, int $background): int @@ -645,7 +645,7 @@ protected function getCol(GdImage $image, float|int $x, float|int $y, int $backg return $background; } - return imagecolorat($image, (int)$x, (int)$y) ?: 0; + return imagecolorat($image, (int) $x, (int) $y) ?: 0; } /** @@ -734,7 +734,7 @@ protected function postEffect(GdImage $image, ?int $bg = null): void protected function rand(int $min, int $max): int { if ($this->useFingerprint) { - $value = (int)current($this->fingerprint); + $value = (int) current($this->fingerprint); $value = max(0, $value); $value = min(255, $value); next($this->fingerprint); @@ -788,7 +788,7 @@ protected function validateBackgroundImage(string $backgroundImage): ?string */ protected function writePhrase(GdImage $image, ?string $phrase, string $font, int $width, int $height): ?int { - $length = mb_strlen((string)$phrase); + $length = mb_strlen((string) $phrase); if ($length === 0 || !$phrase) { return imagecolorallocate($image, 0, 0, 0) ?: null; } From 0e16869a8a2f88190017122de57ef9bea05d4857 Mon Sep 17 00:00:00 2001 From: lachlan Date: Tue, 23 Jun 2026 09:03:25 +1000 Subject: [PATCH 8/9] add and sort to .gitattributes --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index a972456..18233e7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,8 @@ +/demo/ export-ignore /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore -/demo/ export-ignore +/.php-cs-fixer.php export-ignore /phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /tests/ export-ignore From 4d32141f4d563500863ecb67372389991b53a1ba Mon Sep 17 00:00:00 2001 From: lachlan Date: Tue, 23 Jun 2026 09:05:16 +1000 Subject: [PATCH 9/9] treat . as a char --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 18233e7..c633602 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,8 @@ -/demo/ export-ignore /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore /.php-cs-fixer.php export-ignore +/demo/ export-ignore /phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /tests/ export-ignore