sandfoxme/monsterid

View on GitHub
src/classes/Monster.php

Summary

Maintainability
A
0 mins
Test Coverage
<?php

declare(strict_types=1);

namespace Arokettu\MonsterID;

use Arokettu\MonsterID\Randomizer\FactoryInterface;
use DomainException;
use GdImage;
use Http\Discovery\Psr17FactoryDiscovery;
use InvalidArgumentException;
use LogicException;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Random\Randomizer;

use function Arokettu\IsResource\try_get_resource_type;

final class Monster
{
    public const DEFAULT_SIZE = MONSTER_DEFAULT_SIZE;
    public const MIME = MONSTER_MIME;

    private const PARTS_PATH = __DIR__ . '/../../assets/parts';

    private GdImage $monster;
    private FactoryInterface $rngFactory;

    public function __construct(
        private string $string,
        private int $size = self::DEFAULT_SIZE,
        ?FactoryInterface $rngFactory = null
    ) {
        if ($size < 1) {
            throw new DomainException('$size must be 1 or more');
        }

        $this->rngFactory = $rngFactory ?? Config::getRandomizerFactory();
    }

    public function getGdImage(): GdImage
    {
        if (!isset($this->monster)) {
            $this->buildImage();
        }

        $newmonster = imagecreatetruecolor($this->size, $this->size);
        imagecopy($newmonster, $this->monster, 0, 0, 0, 0, $this->size, $this->size);

        return $newmonster;
    }

    /**
     * @param resource|null $stream write png image to string
     * @return resource the same resource
     */
    public function writeToStream($stream = null)
    {
        if ($stream) {
            if (try_get_resource_type($stream) !== 'stream') {
                throw new InvalidArgumentException('$stream should be a writable stream');
            }
        } else {
            $stream = fopen('php://temp', 'r+');
        }

        if (!isset($this->monster)) {
            $this->buildImage();
        }

        imagepng($this->monster, $stream);

        return $stream;
    }

    /**
     * @return string png image content
     */
    public function getImage(): string
    {
        $stream = $this->writeToStream();
        rewind($stream);
        $image = stream_get_contents($stream);
        fclose($stream);

        return $image;
    }

    public function getResponse(
        ?ResponseFactoryInterface $responseFactory = null,
        ?StreamFactoryInterface $streamFactory = null,
    ): ResponseInterface {
        $responseFactory ??= Psr17FactoryDiscovery::findResponseFactory();
        $streamFactory ??= Psr17FactoryDiscovery::findStreamFactory();

        $stream = $this->writeToStream();
        rewind($stream);

        return $responseFactory->createResponse()
            ->withBody($streamFactory->createStreamFromResource($stream))
            ->withHeader('Content-Type', self::MIME);
    }

    private function buildImage(): void
    {
        $randomizer = new Randomizer($this->rngFactory->getRandomizer($this->string));

        $monster = $this->createImage();
        $parts = $this->generateRandomParts($randomizer);

        // add parts
        foreach ($parts as $part => $number) {
            $this->applyPartToImage($monster, $part, $number, $randomizer);
        }

        $this->monster = $this->prepareOutput($monster);
    }

    private function createImage(): GdImage
    {
        // create background
        $monster = imagecreatetruecolor(MONSTER_DEFAULT_SIZE, MONSTER_DEFAULT_SIZE);
        if (!$monster) {
            throw new LogicException('GD image create failed'); // @codeCoverageIgnore
        }
        $white = imagecolorallocate($monster, 255, 255, 255);
        imagefill($monster, 0, 0, $white);

        return $monster;
    }

    private function applyPartToImage(GdImage $monster, string $part, int $number, Randomizer $randomizer): void
    {
        $file = self::PARTS_PATH . DIRECTORY_SEPARATOR . "{$part}_{$number}.png";

        $partImage = imagecreatefrompng($file);
        if (!$partImage) {
            throw new LogicException('Failed to load ' . $file); // @codeCoverageIgnore
        }
        imagesavealpha($partImage, true);
        imagecopy($monster, $partImage, 0, 0, 0, 0, MONSTER_DEFAULT_SIZE, MONSTER_DEFAULT_SIZE);
        imagedestroy($partImage);

        // color the body
        if ($part == 'body') {
            $color = imagecolorallocate(
                $monster,
                $randomizer->getInt(20, 235),
                $randomizer->getInt(20, 235),
                $randomizer->getInt(20, 235)
            );
            imagefill($monster, MONSTER_DEFAULT_SIZE / 2, MONSTER_DEFAULT_SIZE / 2, $color);
        }
    }

    private function prepareOutput(GdImage $monster): GdImage
    {
        // resize if needed, then output
        if ($this->size === MONSTER_DEFAULT_SIZE) {
            return $monster;
        } else {
            $out = imagecreatetruecolor($this->size, $this->size);
            if (!$out) {
                throw new LogicException('GD image create failed'); // @codeCoverageIgnore
            }
            imagecopyresampled($out, $monster, 0, 0, 0, 0, $this->size, $this->size, 120, 120);
            imagedestroy($monster);
            return $out;
        }
    }

    /**
     * @return array<string, int>
     */
    private function generateRandomParts(Randomizer $randomizer): array
    {
        // throw the dice for body parts
        return [
            'legs' =>   $randomizer->getInt(1, 5),
            'hair' =>   $randomizer->getInt(1, 5),
            'arms' =>   $randomizer->getInt(1, 5),
            'body' =>   $randomizer->getInt(1, 15),
            'eyes' =>   $randomizer->getInt(1, 15),
            'mouth' =>  $randomizer->getInt(1, 10),
        ];
    }

    public function __serialize(): array
    {
        return [
            'string'        => $this->string,
            'size'          => $this->size,
            'rngFactory'    => $this->rngFactory,
        ];
    }

    public function __unserialize(array $data): void
    {
        [
            'string'        => $this->string,
            'size'          => $this->size,
            'rngFactory'    => $this->rngFactory,
        ] = $data;
    }
}