diff --git a/app/Captcha/Config/ImageBody.php b/app/Captcha/Config/ImageBody.php new file mode 100644 index 0000000..d557311 --- /dev/null +++ b/app/Captcha/Config/ImageBody.php @@ -0,0 +1,144 @@ +width <= 0) { + throw new CaptchaException('Incorrect $width settings.'); + } + if ($this->height <= 0) { + throw new CaptchaException('Incorrect $height settings.'); + } + if (count($this->backgrounds) < 1) { + throw new CaptchaException('Invalid $backgrounds parameter.'); + } + if (count($this->fonts) < 1) { + throw new CaptchaException('Invalid $fonts parameter.'); + } + if (count($this->fontColors) < 1) { + throw new CaptchaException('Invalid $fontColors parameter.'); + } + if ($this->angle < 0) { + throw new CaptchaException('Incorrect $angle settings.'); + } + + if (!is_integer($this->fontSize) && !is_array($this->fontSize)) { + throw new CaptchaException('The $fontSize parameter is not an array or integer.'); + } elseif (is_array($this->fontSize)) { + if (count($this->fontSize) > 2) { + throw new CaptchaException('The array of $fontSize parameters contains more than 2 keys.'); + } + if ($this->fontSize[0] > $this->fontSize[1]) { + throw new CaptchaException('The number of $fontSize[1] is less than $fontSize[0].'); + } + } + } + + /** + * @return array + */ + public function getBackgrounds(): array + { + return $this->backgrounds; + } + + public function randomBackground(): string + { + return Arr::random($this->getBackgrounds()); + } + + /** + * @return array + */ + public function getFonts(): array + { + return $this->fonts; + } + + public function randomFont(): string + { + return Arr::random($this->getFonts()); + } + + /** + * @return array + */ + public function getFontColors(): array + { + return $this->fontColors; + } + + public function randomFontColor(): string + { + return Arr::random($this->getFontColors()); + } + + /** + * @return int + */ + public function getWidth(): int + { + return $this->width; + } + + /** + * @return int + */ + public function getHeight(): int + { + return $this->height; + } + + /** + * @return int + */ + public function getAngle(): int + { + return $this->angle; + } + + public function randomAngle(): int + { + return mt_rand($this->getAngle() * -1, $this->getAngle()); + } + + public function getNumberLines(): int + { + return $this->numberLines; + } + + public function fontSize(): int + { + $fontSize = $this->fontSize; + if (is_integer($fontSize)) { + return $fontSize; + } + if (empty($fontSize[1])) { + return $fontSize[0]; + } + return random_int($fontSize[0], $fontSize[1]); + } + + /** + * @return array + */ + public function getLineColors(): array + { + return $this->lineColors; + } +} diff --git a/app/Captcha/Config/ImageHead.php b/app/Captcha/Config/ImageHead.php new file mode 100644 index 0000000..55bddd8 --- /dev/null +++ b/app/Captcha/Config/ImageHead.php @@ -0,0 +1,147 @@ +width <= 0) { + throw new CaptchaException('Incorrect $width settings.'); + } + if ($this->height <= 0) { + throw new CaptchaException('Incorrect $height settings.'); + } + if ($this->textPaddingTop < 0) { + throw new CaptchaException('Incorrect $textPaddingTop settings.'); + } + if ($this->textPaddingLeft < 0) { + throw new CaptchaException('Incorrect $textPaddingLeft settings.'); + } + if (count($this->backgrounds) < 1) { + throw new CaptchaException('Invalid $backgrounds parameter.'); + } + if (count($this->fonts) < 1) { + throw new CaptchaException('Invalid $fonts parameter.'); + } + if (count($this->fontColors) < 1) { + throw new CaptchaException('Invalid $fontColors parameter.'); + } + if ($this->angle < 0) { + throw new CaptchaException('Incorrect $angle settings.'); + } + if ($this->numberLines < 0) { + throw new CaptchaException('Incorrect $numberLines settings.'); + } + } + + /** + * @return array + */ + public function getBackgrounds(): array + { + return $this->backgrounds; + } + + public function randomBackground(): string + { + return Arr::random($this->getBackgrounds()); + } + + /** + * @return array + */ + public function getFonts(): array + { + return $this->fonts; + } + + public function randomFont(): string + { + return Arr::random($this->getFonts()); + } + + /** + * @return array + */ + public function getFontColors(): array + { + return $this->fontColors; + } + + public function randomFontColor(): string + { + return Arr::random($this->getFontColors()); + } + + /** + * @return int + */ + public function getWidth(): int + { + return $this->width; + } + + /** + * @return int + */ + public function getHeight(): int + { + return $this->height; + } + + /** + * @return int + */ + public function getTextPaddingTop(): int + { + return $this->textPaddingTop; + } + + /** + * @return int + */ + public function getTextPaddingLeft(): int + { + return $this->textPaddingLeft; + } + + /** + * @return int + */ + public function getAngle(): int + { + return $this->angle; + } + + public function randomAngle(): int + { + return mt_rand($this->getAngle() * -1, $this->getAngle()); + } + + public function getNumberLines(): int + { + return $this->numberLines; + } + + /** + * @return array + */ + public function getLineColors(): array + { + return $this->lineColors; + } +} diff --git a/app/Captcha/Contracts/ImageBody.php b/app/Captcha/Contracts/ImageBody.php new file mode 100644 index 0000000..549363c --- /dev/null +++ b/app/Captcha/Contracts/ImageBody.php @@ -0,0 +1,12 @@ +x1; + } + + /** + * lower left y-coordinate + * @return int + */ + public function getY1(): int + { + return $this->y1; + } + + /** + * lower right x-coordinate + * @return int + */ + public function getX2(): int + { + return $this->x2; + } + + /** + * lower right y-coordinate + * @return int + */ + public function getY2(): int + { + return $this->y2; + } + + /** + * upper right x-coordinate + * @return int + */ + public function getX3(): int + { + return $this->x3; + } + + /** + * upper right y-coordinate + * @return int + */ + public function getY3(): int + { + return $this->y3; + } + + /** + * upper left x-coordinate + * @return int + */ + public function getX4(): int + { + return $this->x4; + } + + /** + * upper left y-coordinate + * @return int + */ + public function getY4(): int + { + return $this->y4; + } +} diff --git a/app/Captcha/Dto/Image.php b/app/Captcha/Dto/Image.php new file mode 100644 index 0000000..0a737f4 --- /dev/null +++ b/app/Captcha/Dto/Image.php @@ -0,0 +1,36 @@ +imageBase64; + } + + /** + * @return int + */ + public function getWidth(): int + { + return $this->width; + } + + /** + * @return int + */ + public function getHeight(): int + { + return $this->height; + } +} diff --git a/app/Captcha/Dto/ImageBody.php b/app/Captcha/Dto/ImageBody.php new file mode 100644 index 0000000..1a99d5a --- /dev/null +++ b/app/Captcha/Dto/ImageBody.php @@ -0,0 +1,27 @@ +image; + } + + /** + * @return Coordinators + */ + public function getCoordinators(): array + { + return $this->coordinators; + } +} diff --git a/app/Captcha/Dto/ImageHead.php b/app/Captcha/Dto/ImageHead.php new file mode 100644 index 0000000..1cd5540 --- /dev/null +++ b/app/Captcha/Dto/ImageHead.php @@ -0,0 +1,18 @@ +image; + } +} diff --git a/app/Captcha/Dto/Sector.php b/app/Captcha/Dto/Sector.php new file mode 100644 index 0000000..e6674b2 --- /dev/null +++ b/app/Captcha/Dto/Sector.php @@ -0,0 +1,45 @@ +x; + } + + /** + * @return int + */ + public function getY(): int + { + return $this->y; + } + + /** + * @return int + */ + public function getWidth(): int + { + return $this->width; + } + + /** + * @return int + */ + public function getHeight(): int + { + return $this->height; + } +} diff --git a/app/Captcha/Dto/Sectors.php b/app/Captcha/Dto/Sectors.php new file mode 100644 index 0000000..0107085 --- /dev/null +++ b/app/Captcha/Dto/Sectors.php @@ -0,0 +1,24 @@ +points[] = new Sector((int) $x, (int) $y, (int) $width, (int) $height); + + return $this; + } + + public function random(): Sector + { + $key = array_rand($this->points); + $sector = $this->points[$key]; + unset($this->points[$key]); + + return $sector; + } +} diff --git a/app/Captcha/Dto/Symbols.php b/app/Captcha/Dto/Symbols.php new file mode 100644 index 0000000..cae19af --- /dev/null +++ b/app/Captcha/Dto/Symbols.php @@ -0,0 +1,38 @@ +success; + } + + /** + * @return array + */ + public function getFakes(): array + { + return $this->fakes; + } + + /** + * @return SymbolType + */ + public function getType(): SymbolType + { + return $this->type; + } +} diff --git a/app/Captcha/Enums/SymbolType.php b/app/Captcha/Enums/SymbolType.php new file mode 100644 index 0000000..b7ebb5e --- /dev/null +++ b/app/Captcha/Enums/SymbolType.php @@ -0,0 +1,8 @@ +imageManager->createImage($config->getWidth(), $config->getHeight()); + $image->insertBackground($config->randomBackground()); + + $imageData = match ($symbols->getType()) { + SymbolType::String => $this->processingString($image, $symbols, $config), + }; + $image = $imageData['image']; + + if ($config->getNumberLines() > 0) { + $this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines()); + } + + $image = new ImageDto( + imageBase64: $image->encode(), + width: $image->getWidth(), + height: $image->getHeight() + ); + + return new ImageBodyDto( + image: $image, + coordinators: $imageData['coordinators'] + ); + } + + private function processingString(Image $image, Symbols $symbols, ImageBodyConfig $config): array + { + $sectors = $this->calculateSectors($symbols, $image->getWidth(), $image->getHeight()); + $coordinators = []; + foreach ($symbols->getSuccess() as $number => $symbol) { + $coordinators[] = $this->processingStringSymbol($image, $symbol, $config, $sectors->random()); + } + + if (!empty($symbols->getFakes())) { + foreach ($symbols->getFakes() as $number => $symbol) { + $this->processingStringSymbol($image, $symbol, $config, $sectors->random()); + } + } + + return [ + 'image' => $image, + 'coordinators' => $coordinators + ]; + } + + private function processingStringSymbol(Image &$image, string|int $symbol, ImageBodyConfig $config, Sector $sector): Coordinators + { + $fontSize = $config->fontSize(); + if ($fontSize > $sector->getHeight()) { + $fontSize = $sector->getHeight(); + } + $fontPathFile = $config->randomFont(); + $fontColor = $config->randomFontColor(); + $angle = $config->randomAngle(); + $marginLeft = mt_rand($sector->getX(), $sector->getX() + $sector->getWidth() - $fontSize); + $marginTop = mt_rand($sector->getY(), $sector->getY() + $sector->getHeight() - $fontSize); + + return $image->addText( + text: $symbol, + x: $marginLeft, + y: $marginTop, + size: $fontSize, + angle: $angle, + hexColor: $fontColor, + fontName: $fontPathFile + ); + } + + private function calculateSectors(Symbols $symbols, int $width, int $height): Sectors + { + $points = count($symbols->getSuccess()) + count($symbols->getFakes()); + $sumFloors = 3; + $sumRooms = ceil($points / $sumFloors); + + $heightFloor = floor($height / $sumFloors); + $widthFloor = floor($width / $sumRooms); + + $sectors = new Sectors(); + for ($floor = 0; $floor < $sumFloors; $floor++) { + $y = $heightFloor * $floor; + for ($room = 0; $room < $sumRooms; $room++) { + $sectors->add( + x: ($widthFloor * $room), + y: $y, + width: $widthFloor, + height: $heightFloor + ); + } + } + + return $sectors; + } +} diff --git a/app/Captcha/Images/Head.php b/app/Captcha/Images/Head.php new file mode 100644 index 0000000..729f855 --- /dev/null +++ b/app/Captcha/Images/Head.php @@ -0,0 +1,72 @@ +imageManager->createImage($config->getWidth(), $config->getHeight()); + $image->insertBackground($config->randomBackground()); + + $image = match ($symbols->getType()) { + SymbolType::String => $this->processingString($image, $symbols, $config), + }; + if ($config->getNumberLines() > 0) { + $this->imageLines->processing(image: $image, colors: $config->getLineColors(), lines: $config->getNumberLines()); + } + + $image = new ImageDto( + imageBase64: $image->encode(), + width: $image->getWidth(), + height: $image->getHeight() + ); + + return new ImageHeadDto(image: $image); + } + + private function processingString(ImageContract $image, Symbols $symbols, ImageHeadConfig $config): Image + { + $textLeftPadding = $config->getTextPaddingLeft(); + $textTopPadding = $config->getTextPaddingTop(); + $countSymbols = count($symbols->getSuccess()); + $widthPrefix = $image->getWidth() - $textLeftPadding; + $imageHeight = $image->getHeight() - $textTopPadding; + + foreach ($symbols->getSuccess() as $number => $symbol) { + $marginLeft = $textLeftPadding + $number * $widthPrefix / $countSymbols; + + $image->addText( + text: $symbol, + x: intval($marginLeft), + y: $textTopPadding, + size: $this->randomFontSize($imageHeight), + angle: $config->randomAngle(), + hexColor: $config->randomFontColor(), + fontName: $config->randomFont() + ); + } + + return $image; + } + + private function randomFontSize(int $imageHeight): int + { + return mt_rand($imageHeight - 10, $imageHeight); + } +} diff --git a/app/Captcha/Type.php b/app/Captcha/Type.php new file mode 100644 index 0000000..c99ef00 --- /dev/null +++ b/app/Captcha/Type.php @@ -0,0 +1,26 @@ +config; + } + + final public function getConfigValue(string | int $name, mixed $default = null): mixed + { + $config = $this->getConfig(); + return $config[$name] ?? $default; + } +} diff --git a/app/Captcha/Types/StringType.php b/app/Captcha/Types/StringType.php new file mode 100644 index 0000000..121993a --- /dev/null +++ b/app/Captcha/Types/StringType.php @@ -0,0 +1,104 @@ +lengthSymbols(); + $lengthFakeSymbols = $this->lengthFakeSymbols(); + + $success = $this->randomSymbols($lengthSymbols); + $fakes = []; + if (!empty($lengthFakeSymbols)) { + $fakes = $this->randomSymbols($lengthFakeSymbols, $fakes); + } + + return new Symbols( + success: $success, + fakes: $fakes, + type: SymbolType::String + ); + } + + /** + * @param int $min + * @param int $max + * @param array $except + * @return array + * @throws CaptchaException + */ + private function randomSymbols(int $lenght, array $except = []): array + { + $symbols = $this->symbols(); + if (!empty($except)) { + $symbols = array_diff($symbols, $except); + } + + if (count($symbols) < $lenght) { + throw new CaptchaException('The number of characters is less than $lenght.'); + } + + return Arr::random($symbols, $lenght); + } + + /** + * @return array + * @throws CaptchaException + */ + private function symbols(): array + { + $symbols = $this->getConfigValue('symbols'); + if (!is_array($symbols)) { + throw new CaptchaException('The symbols parameter is not an array.'); + } + return $symbols; + } + + private function lengthSymbols(): int + { + $length = $this->getConfigValue('length_symbols'); + if (is_integer($length)) { + return $length; + } + if (!is_array($length)) { + throw new CaptchaException('The length_symbols parameter is not an array or integer.'); + } + if (count($length) > 2) { + throw new CaptchaException('The array of length_symbols parameters contains more than 2 keys.'); + } + if ($length[0] > $length[1]) { + throw new CaptchaException('The number of length_symbols[1] is less than length_symbols[0].'); + } + return random_int($length[0], $length[1]); + } + + private function lengthFakeSymbols(): int + { + $length = $this->getConfigValue('length_fake_symbols'); + if (is_integer($length)) { + return $length; + } + if (is_null($length) || $length === false) { + return 0; + } + if (!is_array($length)) { + throw new CaptchaException('The length_fake_symbols parameter is not an array or integer or null or false.'); + } + if (count($length) > 2) { + throw new CaptchaException('The array of length_fake_symbols parameters contains more than 2 keys.'); + } + if ($length[0] > $length[1]) { + throw new CaptchaException('The number of length_fake_symbols[1] is less than length_fake_symbols[0].'); + } + return random_int($length[0], $length[1]); + } +} diff --git a/app/Http/Controllers/Api/V1/CaptchaController.php b/app/Http/Controllers/Api/V1/CaptchaController.php new file mode 100644 index 0000000..1e1f3f9 --- /dev/null +++ b/app/Http/Controllers/Api/V1/CaptchaController.php @@ -0,0 +1,24 @@ +captchaService->generate(); + if (!$result->isSuccess()) { + return response()->json($result->getData())->setStatusCode($result->getCode()); + } + + return response()->json(new Captcha($result)); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Api/V1/Controller.php similarity index 78% rename from app/Http/Controllers/Controller.php rename to app/Http/Controllers/Api/V1/Controller.php index 77ec359..8075c0e 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Api/V1/Controller.php @@ -1,6 +1,6 @@ - $this->resource->getImageBase64(), + 'image_text_base64' => $this->resource->getImageTextBase64(), + 'captcha_key' => $this->resource->getKey() + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..4d778a2 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -1,17 +1,40 @@ -app->bind(ImageManagerContract::class, function () { + return new ImageManager(imageClassName: config('captcha.imageClass')); + }); + $this->app->bind(ImageHead::class, Head::class); + $this->app->bind(ImageBody::class, Body::class); + $this->app->bind(ImageLines::class, Lines::class); + + $this->app->bind(CaptchaService::class, function (Application $app) { + return new CaptchaService( + config: config('captcha', []), + imageHead: $app->make(ImageHead::class), + imageBody: $app->make(ImageBody::class) + ); + }); } /** diff --git a/app/ServiceResults/Api/V1/CaptchaService/Captcha.php b/app/ServiceResults/Api/V1/CaptchaService/Captcha.php new file mode 100644 index 0000000..79edf7c --- /dev/null +++ b/app/ServiceResults/Api/V1/CaptchaService/Captcha.php @@ -0,0 +1,38 @@ +imageBase64; + } + + /** + * @return string + */ + public function getImageTextBase64(): string + { + return $this->imageTextBase64; + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } +} diff --git a/app/Services/Api/V1/CaptchaService.php b/app/Services/Api/V1/CaptchaService.php new file mode 100644 index 0000000..33553d2 --- /dev/null +++ b/app/Services/Api/V1/CaptchaService.php @@ -0,0 +1,97 @@ +config['types'] ?? []; + if (empty($types)) { + $error = __('No captcha type settings!'); + report($error); + return $this->errService($error); + } + + try { + $type = Arr::random($types); + + /** @var Type $captcha */ + $typeCaptcha = new $type['class']( + $type['params'] ?? [] + ); + $symbols = $typeCaptcha->getSymbols(); + + $imageHeadConfig = $this->makeImageHeadConfig($this->config['image_head'] ?? []); + $imageHead = $this->imageHead->processing($symbols, $imageHeadConfig); + unset($imageHeadConfig); + + $imageBodyConfig = $this->makeImageBodyConfig($this->config['image_body'] ?? []); + $imageBody = $this->imageBody->processing($symbols, $imageBodyConfig); + unset($imageBodyConfig); + + + dd($imageHead, $imageBody); + } catch (\Throwable $e) { + report($e); + return $this->errService('Captcha service error!'); + } + + + + + + return new Captcha( + imageBase64: $imageHead->getImage()->getImageBase64(), + imageTextBase64: $imageBody->getImage()->getImageBase64(), + key: 'dddd' + ); + } + + private function makeImageHeadConfig(array $params): ImageHeadConfig + { + return new ImageHeadConfig( + backgrounds: $params['backgrounds'] ?? [], + fonts: $params['fonts'] ?? [], + fontColors: $params['font_colors'] ?? [], + width: $params['width'] ?? 150, + height: $params['height'] ?? 40, + textPaddingTop: $params['text_padding_top'] ?? 5, + textPaddingLeft: $params['text_padding_left'] ?? 10, + angle: $params['angle'] ?? 20, + numberLines: $params['number_lines'] ?? 3, + lineColors: $params['line_colors'] ?? [] + ); + } + + private function makeImageBodyConfig(array $params): ImageBodyConfig + { + return new ImageBodyConfig( + backgrounds: $params['backgrounds'] ?? [], + fonts: $params['fonts'] ?? [], + fontColors: $params['font_colors'] ?? [], + width: $params['width'] ?? 300, + height: $params['height'] ?? 240, + angle: $params['angle'] ?? 20, + fontSize: $params['font_size'] ?? [20, 50], + numberLines: $params['number_lines'] ?? 3, + lineColors: $params['line_colors'] ?? [] + ); + } +} diff --git a/app/Services/Service.php b/app/Services/Service.php new file mode 100644 index 0000000..10fb348 --- /dev/null +++ b/app/Services/Service.php @@ -0,0 +1,54 @@ +error(422, $message, $errors); + } + + final protected function errFobidden(string $message): ServiceResultError + { + return $this->error(403, $message); + } + + final protected function errNotFound(string $message): ServiceResultError + { + return $this->error(404, $message); + } + + final protected function errService(string $message): ServiceResultError + { + return $this->error(500, $message); + } + + final protected function notAcceptable(string $message): ServiceResultError + { + return $this->error(406, $message); + } + + final protected function ok(string $message = 'OK'): ServiceResultArray + { + return $this->result(['message' => $message]); + } + + final protected function result(array $data = []): ServiceResultArray + { + return new ServiceResultArray(data: $data); + } + + final protected function error(int $code, string $message, array $errors = []): ServiceResultError + { + return new ServiceResultError( + message: $message, + errors: $errors, + code: $code + ); + } +} diff --git a/routes/api-v1.php b/routes/api-v1.php index fa84410..a6295b9 100644 --- a/routes/api-v1.php +++ b/routes/api-v1.php @@ -13,3 +13,5 @@ use Illuminate\Support\Facades\Route; | be assigned to the "api" middleware group. Make something great! | */ + +Route::get('/captcha', [\App\Http\Controllers\Api\V1\CaptchaController::class, 'getCaptcha']);