1<?php
2declare(strict_types = 1);
3
4namespace BaconQrCode\Renderer;
5
6use BaconQrCode\Encoder\MatrixUtil;
7use BaconQrCode\Encoder\QrCode;
8use BaconQrCode\Exception\InvalidArgumentException;
9use BaconQrCode\Renderer\Image\ImageBackEndInterface;
10use BaconQrCode\Renderer\Path\Path;
11use BaconQrCode\Renderer\RendererStyle\EyeFill;
12use BaconQrCode\Renderer\RendererStyle\RendererStyle;
13
14final class ImageRenderer implements RendererInterface
15{
16    /**
17     * @var RendererStyle
18     */
19    private $rendererStyle;
20
21    /**
22     * @var ImageBackEndInterface
23     */
24    private $imageBackEnd;
25
26    public function __construct(RendererStyle $rendererStyle, ImageBackEndInterface $imageBackEnd)
27    {
28        $this->rendererStyle = $rendererStyle;
29        $this->imageBackEnd = $imageBackEnd;
30    }
31
32    /**
33     * @throws InvalidArgumentException if matrix width doesn't match height
34     */
35    public function render(QrCode $qrCode) : string
36    {
37        $size = $this->rendererStyle->getSize();
38        $margin = $this->rendererStyle->getMargin();
39        $matrix = $qrCode->getMatrix();
40        $matrixSize = $matrix->getWidth();
41
42        if ($matrixSize !== $matrix->getHeight()) {
43            throw new InvalidArgumentException('Matrix must have the same width and height');
44        }
45
46        $totalSize = $matrixSize + ($margin * 2);
47        $moduleSize = $size / $totalSize;
48        $fill = $this->rendererStyle->getFill();
49
50        $this->imageBackEnd->new($size, $fill->getBackgroundColor());
51        $this->imageBackEnd->scale((float) $moduleSize);
52        $this->imageBackEnd->translate((float) $margin, (float) $margin);
53
54        $module = $this->rendererStyle->getModule();
55        $moduleMatrix = clone $matrix;
56        MatrixUtil::removePositionDetectionPatterns($moduleMatrix);
57        $modulePath = $this->drawEyes($matrixSize, $module->createPath($moduleMatrix));
58
59        if ($fill->hasGradientFill()) {
60            $this->imageBackEnd->drawPathWithGradient(
61                $modulePath,
62                $fill->getForegroundGradient(),
63                0,
64                0,
65                $matrixSize,
66                $matrixSize
67            );
68        } else {
69            $this->imageBackEnd->drawPathWithColor($modulePath, $fill->getForegroundColor());
70        }
71
72        return $this->imageBackEnd->done();
73    }
74
75    private function drawEyes(int $matrixSize, Path $modulePath) : Path
76    {
77        $fill = $this->rendererStyle->getFill();
78
79        $eye = $this->rendererStyle->getEye();
80        $externalPath = $eye->getExternalPath();
81        $internalPath = $eye->getInternalPath();
82
83        $modulePath = $this->drawEye(
84            $externalPath,
85            $internalPath,
86            $fill->getTopLeftEyeFill(),
87            3.5,
88            3.5,
89            0,
90            $modulePath
91        );
92        $modulePath = $this->drawEye(
93            $externalPath,
94            $internalPath,
95            $fill->getTopRightEyeFill(),
96            $matrixSize - 3.5,
97            3.5,
98            90,
99            $modulePath
100        );
101        $modulePath = $this->drawEye(
102            $externalPath,
103            $internalPath,
104            $fill->getBottomLeftEyeFill(),
105            3.5,
106            $matrixSize - 3.5,
107            -90,
108            $modulePath
109        );
110
111        return $modulePath;
112    }
113
114    private function drawEye(
115        Path $externalPath,
116        Path $internalPath,
117        EyeFill $fill,
118        float $xTranslation,
119        float $yTranslation,
120        int $rotation,
121        Path $modulePath
122    ) : Path {
123        if ($fill->inheritsBothColors()) {
124            return $modulePath
125                ->append($externalPath->translate($xTranslation, $yTranslation))
126                ->append($internalPath->translate($xTranslation, $yTranslation));
127        }
128
129        $this->imageBackEnd->push();
130        $this->imageBackEnd->translate($xTranslation, $yTranslation);
131
132        if (0 !== $rotation) {
133            $this->imageBackEnd->rotate($rotation);
134        }
135
136        if ($fill->inheritsExternalColor()) {
137            $modulePath = $modulePath->append($externalPath->translate($xTranslation, $yTranslation));
138        } else {
139            $this->imageBackEnd->drawPathWithColor($externalPath, $fill->getExternalColor());
140        }
141
142        if ($fill->inheritsInternalColor()) {
143            $modulePath = $modulePath->append($internalPath->translate($xTranslation, $yTranslation));
144        } else {
145            $this->imageBackEnd->drawPathWithColor($internalPath, $fill->getInternalColor());
146        }
147
148        $this->imageBackEnd->pop();
149
150        return $modulePath;
151    }
152}
153