Вход Регистрация
Файл: vendor/mobicms/captcha/src/Image.php
Строк: 333
<?php

declare(strict_types=1);

namespace 
MobicmsCaptcha;

use 
GdImage;
use 
InvalidArgumentException;
use 
LogicException;
use 
RandomRandomException;
use 
RuntimeException;

use function 
base64_encode;
use function 
basename;
use function 
count;
use function 
imagecolorallocate;
use function 
imagecolorallocatealpha;
use function 
imagecreatetruecolor;
use function 
imagefill;
use function 
imagepng;
use function 
imagesavealpha;
use function 
imagettftext;
use function 
ob_get_clean;
use function 
ob_start;
use function 
preg_match;
use function 
random_int;
use function 
strtolower;
use function 
strtoupper;

/**
 * @psalm-api
 */
final class Image
{
    
/** @var array<string> */
    
private array $fontList = [];
    private 
string $code;

    public const 
FONT_CASE_UPPER = 2;
    public const 
FONT_CASE_LOWER = 1;
    public const 
ALPHA_TRANSPARENT = 127;
    public const 
COLOR_MAX = 150;
    public const 
COLOR_MIN = 0;

    
////////////////////////////////////////////////////////////
    // Image options                                          //
    ////////////////////////////////////////////////////////////
    
public string $imageFormat = 'png'; // png, gif, webp
    
public int $imageWidth = 190;
    public 
int $imageHeight = 90;
    public 
int $defaultFontSize = 30;
    public 
bool $fontMix = true;

    
/** @var array<string> */
    
public array $fontFolders = [__DIR__ . '/../fonts'];

    
/** @var array<string, array<string, int>> */
    
public array $fontsTune = [
        
'3dlet.ttf'          => [
            
'size' => 16,
            
'case' => self::FONT_CASE_LOWER,
        ],
        
'baby_blocks.ttf'    => [
            
'size' => -8,
        ],
        
'karmaticarcade.ttf' => [
            
'size' => -4,
        ],
        
'betsy_flanagan.ttf' => [
            
'size' => 4,
        ],
    ];

    
////////////////////////////////////////////////////////////
    // Code options                                           //
    ////////////////////////////////////////////////////////////
    
public int $lengthMin = 4;
    public 
int $lengthMax = 5;
    public 
string $characterSet = '23456789ABCDEGHJKMNPQRSTUVXYZabcdeghjkmnpqrstuvxyz';
    public 
string $excludedCombinationsPattern = 'rn|rm|mm|ww';

    public function 
__construct(string $code = '')
    {
        
$this->code = $code;
    }

    
/**
     * @throws RandomException
     */
    
public function getCode(): string
    
{
        return 
$this->code === ''
            
? $this->code = $this->generateRandomString()
            : 
$this->code;
    }

    
/**
     * Generates a CAPTCHA image as a base64-encoded PNG.
     *
     * @return string Base64-encoded PNG image data URI.
     * @throws RandomException If image creation fails or no fonts are found.
     */
    
public function getImage(): string
    
{
        return 
'data:image/' . $this->imageFormat . ';base64,' . base64_encode($this->build());
    }

    
/**
     * @throws RuntimeException|RandomException
     */
    
public function build(): string
    
{
        
$this->fontList = $this->getFontsList();
        
$image = $this->createBaseImage();
        
$backgroundColor = $this->allocateBackgroundColor($image);
        
imagesavealpha($image, true);
        
imagecolortransparent($image, $backgroundColor);
        
imagefill($image, 0, 0, $backgroundColor);
        
$image = $this->drawText($image);

        
ob_start();

        
match ($this->imageFormat) {
            
'gif' => imagegif($image),
            
'png' => imagepng($image),
            
'webp' => imagewebp($image),
            default => (function () {
                
ob_end_clean();
                throw new 
InvalidArgumentException('Unsupported image format "' . $this->imageFormat . '".');
            })()
        };

        return (string) 
ob_get_clean();
    }

    private function 
createBaseImage(): GdImage
    
{
        
$image = imagecreatetruecolor($this->imageWidth, $this->imageHeight);

        if (
$image === false) {
            
// @codeCoverageIgnoreStart
            
throw new RuntimeException('Failed to create image.');
            
// @codeCoverageIgnoreEnd
        
}

        return 
$image;
    }

    private function 
allocateBackgroundColor(GdImage $image): int
    
{
        
$backgroundColor = imagecolorallocatealpha($image, 240, 240, 240, self::ALPHA_TRANSPARENT);

        if (
$backgroundColor === false) {
            
// @codeCoverageIgnoreStart
            
throw new RuntimeException('Failed to allocate background color.');
            
// @codeCoverageIgnoreEnd
        
}

        return 
$backgroundColor;
    }

    
/**
     * Draws the captcha text on the image using random font, size, color, and angle.
     *
     * @throws RandomException
     */
    
private function drawText(GdImage $image): GdImage
    
{
        
$font = $this->getRandomFont();
        
$symbols = str_split($this->getCode());
        
$len = count($symbols);

        foreach (
$symbols as $key => $symbol) {
            if (
$this->fontMix) {
                
$font = $this->getRandomFont();
            }

            
$fontName = basename($font);
            
$letter = $this->setLetterCase($symbol, $fontName);
            
$fontSize = $this->getFontSize($fontName);
            
$xPos = (int) (($this->imageWidth - $fontSize) / $len) * $key + (int) ($fontSize / 2);
            
$xPos = random_int($xPos, $xPos + 5);
            
$yPos = $this->imageHeight - (int) (($this->imageHeight - $fontSize) / 2);
            
$angle = random_int(-25, 25);
            
$textColor = imagecolorallocate(
                
$image,
                
$this->getRandomColor(),
                
$this->getRandomColor(),
                
$this->getRandomColor()
            );

            if (
$textColor !== false) {
                
imagettftext($image, $fontSize, $angle, $xPos, $yPos, $textColor, $font, $letter);
            }
        }

        return 
$image;
    }

    
/**
     * Adjusts the case of a letter based on font-specific settings.
     */
    
private function setLetterCase(string $string, string $fontName): string
    
{
        return 
match ($this->fontsTune[$fontName]['case'] ?? 0) {
            
self::FONT_CASE_UPPER => strtoupper($string),
            
self::FONT_CASE_LOWER => strtolower($string),
            default => 
$string,
        };
    }

    
/**
     * Retrieves the list of font files from the specified directories.
     *
     * @return array<string>
     */
    
private function getFontsList(): array
    {
        
$fonts = [];

        foreach (
$this->fontFolders as $folder) {
            
$list = glob($folder . DIRECTORY_SEPARATOR . '*.ttf');

            if ([] === 
$list || false === $list) {
                throw new 
LogicException('The specified folder "' . $folder . '" does not contain any fonts.');
            }

            
$fonts = array_merge($fonts, $list);
        }

        return 
$fonts;
    }

    
/**
     * Calculates the font size based on default size and font-specific tuning.
     */
    
private function getFontSize(string $fontName): int
    
{
        return isset(
$this->fontsTune[$fontName]['size'])
            ? 
$this->defaultFontSize + $this->fontsTune[$fontName]['size']
            : 
$this->defaultFontSize;
    }

    
/**
     * @throws RandomException
     */
    
private function getRandomFont(): string
    
{
        return 
$this->fontList[random_int(0, count($this->fontList) - 1)];
    }

    
/**
     * @throws RandomException
     */
    
private function getRandomColor(): int
    
{
        return 
random_int(self::COLOR_MIN, self::COLOR_MAX);
    }

    
/**
     * @param int $count This parameter eliminates infinite recursion in some cases.
     * @throws RandomException
     */
    
private function generateRandomString(int $count = 1): string
    
{
        
$str = '';
        
$length = random_int($this->lengthMin, $this->lengthMax);
        
$characters = str_split($this->characterSet);

        for (
$i = 0; $i < $length; $i++) {
            
$str .= $characters[random_int(0, count($characters) - 1)];
        }

        return 
$count < 10 && preg_match('/' . $this->excludedCombinationsPattern . '/', $str)
            ? 
$this->generateRandomString(++$count)
            : 
$str;
    }
}
Онлайн: 2
Реклама