1<?php
2declare(strict_types = 1);
3
4namespace BaconQrCode\Renderer\Image;
5
6use BaconQrCode\Exception\RuntimeException;
7use BaconQrCode\Renderer\Color\Alpha;
8use BaconQrCode\Renderer\Color\Cmyk;
9use BaconQrCode\Renderer\Color\ColorInterface;
10use BaconQrCode\Renderer\Color\Gray;
11use BaconQrCode\Renderer\Color\Rgb;
12use BaconQrCode\Renderer\Path\Close;
13use BaconQrCode\Renderer\Path\Curve;
14use BaconQrCode\Renderer\Path\EllipticArc;
15use BaconQrCode\Renderer\Path\Line;
16use BaconQrCode\Renderer\Path\Move;
17use BaconQrCode\Renderer\Path\Path;
18use BaconQrCode\Renderer\RendererStyle\Gradient;
19use BaconQrCode\Renderer\RendererStyle\GradientType;
20
21final class EpsImageBackEnd implements ImageBackEndInterface
22{
23    private const PRECISION = 3;
24
25    /**
26     * @var string|null
27     */
28    private $eps;
29
30    public function new(int $size, ColorInterface $backgroundColor) : void
31    {
32        $this->eps = "%!PS-Adobe-3.0 EPSF-3.0\n"
33            . "%%Creator: BaconQrCode\n"
34            . sprintf("%%%%BoundingBox: 0 0 %d %d \n", $size, $size)
35            . "%%BeginProlog\n"
36            . "save\n"
37            . "50 dict begin\n"
38            . "/q { gsave } bind def\n"
39            . "/Q { grestore } bind def\n"
40            . "/s { scale } bind def\n"
41            . "/t { translate } bind def\n"
42            . "/r { rotate } bind def\n"
43            . "/n { newpath } bind def\n"
44            . "/m { moveto } bind def\n"
45            . "/l { lineto } bind def\n"
46            . "/c { curveto } bind def\n"
47            . "/z { closepath } bind def\n"
48            . "/f { eofill } bind def\n"
49            . "/rgb { setrgbcolor } bind def\n"
50            . "/cmyk { setcmykcolor } bind def\n"
51            . "/gray { setgray } bind def\n"
52            . "%%EndProlog\n"
53            . "1 -1 s\n"
54            . sprintf("0 -%d t\n", $size);
55
56        if ($backgroundColor instanceof Alpha && 0 === $backgroundColor->getAlpha()) {
57            return;
58        }
59
60        $this->eps .= wordwrap(
61            '0 0 m'
62            . sprintf(' %s 0 l', (string) $size)
63            . sprintf(' %s %s l', (string) $size, (string) $size)
64            . sprintf(' 0 %s l', (string) $size)
65            . ' z'
66            . ' ' .$this->getColorSetString($backgroundColor) . " f\n",
67            75,
68            "\n "
69        );
70    }
71
72    public function scale(float $size) : void
73    {
74        if (null === $this->eps) {
75            throw new RuntimeException('No image has been started');
76        }
77
78        $this->eps .= sprintf("%1\$s %1\$s s\n", round($size, self::PRECISION));
79    }
80
81    public function translate(float $x, float $y) : void
82    {
83        if (null === $this->eps) {
84            throw new RuntimeException('No image has been started');
85        }
86
87        $this->eps .= sprintf("%s %s t\n", round($x, self::PRECISION), round($y, self::PRECISION));
88    }
89
90    public function rotate(int $degrees) : void
91    {
92        if (null === $this->eps) {
93            throw new RuntimeException('No image has been started');
94        }
95
96        $this->eps .= sprintf("%d r\n", $degrees);
97    }
98
99    public function push() : void
100    {
101        if (null === $this->eps) {
102            throw new RuntimeException('No image has been started');
103        }
104
105        $this->eps .= "q\n";
106    }
107
108    public function pop() : void
109    {
110        if (null === $this->eps) {
111            throw new RuntimeException('No image has been started');
112        }
113
114        $this->eps .= "Q\n";
115    }
116
117    public function drawPathWithColor(Path $path, ColorInterface $color) : void
118    {
119        if (null === $this->eps) {
120            throw new RuntimeException('No image has been started');
121        }
122
123        $fromX = 0;
124        $fromY = 0;
125        $this->eps .= wordwrap(
126            'n '
127            . $this->drawPathOperations($path, $fromX, $fromY)
128            . ' ' . $this->getColorSetString($color) . " f\n",
129            75,
130            "\n "
131        );
132    }
133
134    public function drawPathWithGradient(
135        Path $path,
136        Gradient $gradient,
137        float $x,
138        float $y,
139        float $width,
140        float $height
141    ) : void {
142        if (null === $this->eps) {
143            throw new RuntimeException('No image has been started');
144        }
145
146        $fromX = 0;
147        $fromY = 0;
148        $this->eps .= wordwrap(
149            'q n ' . $this->drawPathOperations($path, $fromX, $fromY) . "\n",
150            75,
151            "\n "
152        );
153
154        $this->createGradientFill($gradient, $x, $y, $width, $height);
155    }
156
157    public function done() : string
158    {
159        if (null === $this->eps) {
160            throw new RuntimeException('No image has been started');
161        }
162
163        $this->eps .= "%%TRAILER\nend restore\n%%EOF";
164        $blob = $this->eps;
165        $this->eps = null;
166
167        return $blob;
168    }
169
170    private function drawPathOperations(Iterable $ops, &$fromX, &$fromY) : string
171    {
172        $pathData = [];
173
174        foreach ($ops as $op) {
175            switch (true) {
176                case $op instanceof Move:
177                    $fromX = $toX = round($op->getX(), self::PRECISION);
178                    $fromY = $toY = round($op->getY(), self::PRECISION);
179                    $pathData[] = sprintf('%s %s m', $toX, $toY);
180                    break;
181
182                case $op instanceof Line:
183                    $fromX = $toX = round($op->getX(), self::PRECISION);
184                    $fromY = $toY = round($op->getY(), self::PRECISION);
185                    $pathData[] = sprintf('%s %s l', $toX, $toY);
186                    break;
187
188                case $op instanceof EllipticArc:
189                    $pathData[] = $this->drawPathOperations($op->toCurves($fromX, $fromY), $fromX, $fromY);
190                    break;
191
192                case $op instanceof Curve:
193                    $x1 = round($op->getX1(), self::PRECISION);
194                    $y1 = round($op->getY1(), self::PRECISION);
195                    $x2 = round($op->getX2(), self::PRECISION);
196                    $y2 = round($op->getY2(), self::PRECISION);
197                    $fromX = $x3 = round($op->getX3(), self::PRECISION);
198                    $fromY = $y3 = round($op->getY3(), self::PRECISION);
199                    $pathData[] = sprintf('%s %s %s %s %s %s c', $x1, $y1, $x2, $y2, $x3, $y3);
200                    break;
201
202                case $op instanceof Close:
203                    $pathData[] = 'z';
204                    break;
205
206                default:
207                    throw new RuntimeException('Unexpected draw operation: ' . get_class($op));
208            }
209        }
210
211        return implode(' ', $pathData);
212    }
213
214    private function createGradientFill(Gradient $gradient, float $x, float $y, float $width, float $height) : void
215    {
216        $startColor = $gradient->getStartColor();
217        $endColor = $gradient->getEndColor();
218
219        if ($startColor instanceof Alpha) {
220            $startColor = $startColor->getBaseColor();
221        }
222
223        $startColorType = get_class($startColor);
224
225        if (! in_array($startColorType, [Rgb::class, Cmyk::class, Gray::class])) {
226            $startColorType = Cmyk::class;
227            $startColor = $startColor->toCmyk();
228        }
229
230        if (get_class($endColor) !== $startColorType) {
231            switch ($startColorType) {
232                case Cmyk::class:
233                    $endColor = $endColor->toCmyk();
234                    break;
235
236                case Rgb::class:
237                    $endColor = $endColor->toRgb();
238                    break;
239
240                case Gray::class:
241                    $endColor = $endColor->toGray();
242                    break;
243            }
244        }
245
246        $this->eps .= "eoclip\n<<\n";
247
248        if ($gradient->getType() === GradientType::RADIAL()) {
249            $this->eps .= " /ShadingType 3\n";
250        } else {
251            $this->eps .= " /ShadingType 2\n";
252        }
253
254        $this->eps .= " /Extend [ true true ]\n"
255            . " /AntiAlias true\n";
256
257        switch ($startColorType) {
258            case Cmyk::class:
259                $this->eps .= " /ColorSpace /DeviceCMYK\n";
260                break;
261
262            case Rgb::class:
263                $this->eps .= " /ColorSpace /DeviceRGB\n";
264                break;
265
266            case Gray::class:
267                $this->eps .= " /ColorSpace /DeviceGray\n";
268                break;
269        }
270
271        switch ($gradient->getType()) {
272            case GradientType::HORIZONTAL():
273                $this->eps .= sprintf(
274                    " /Coords [ %s %s %s %s ]\n",
275                    round($x, self::PRECISION),
276                    round($y, self::PRECISION),
277                    round($x + $width, self::PRECISION),
278                    round($y, self::PRECISION)
279                );
280                break;
281
282            case GradientType::VERTICAL():
283                $this->eps .= sprintf(
284                    " /Coords [ %s %s %s %s ]\n",
285                    round($x, self::PRECISION),
286                    round($y, self::PRECISION),
287                    round($x, self::PRECISION),
288                    round($y + $height, self::PRECISION)
289                );
290                break;
291
292            case GradientType::DIAGONAL():
293                $this->eps .= sprintf(
294                    " /Coords [ %s %s %s %s ]\n",
295                    round($x, self::PRECISION),
296                    round($y, self::PRECISION),
297                    round($x + $width, self::PRECISION),
298                    round($y + $height, self::PRECISION)
299                );
300                break;
301
302            case GradientType::INVERSE_DIAGONAL():
303                $this->eps .= sprintf(
304                    " /Coords [ %s %s %s %s ]\n",
305                    round($x, self::PRECISION),
306                    round($y + $height, self::PRECISION),
307                    round($x + $width, self::PRECISION),
308                    round($y, self::PRECISION)
309                );
310                break;
311
312            case GradientType::RADIAL():
313                $centerX = ($x + $width) / 2;
314                $centerY = ($y + $height) / 2;
315
316                $this->eps .= sprintf(
317                    " /Coords [ %s %s 0 %s %s %s ]\n",
318                    round($centerX, self::PRECISION),
319                    round($centerY, self::PRECISION),
320                    round($centerX, self::PRECISION),
321                    round($centerY, self::PRECISION),
322                    round(max($width, $height) / 2, self::PRECISION)
323                );
324                break;
325        }
326
327        $this->eps .= " /Function\n"
328            . " <<\n"
329            . "  /FunctionType 2\n"
330            . "  /Domain [ 0 1 ]\n"
331            . sprintf("  /C0 [ %s ]\n", $this->getColorString($startColor))
332            . sprintf("  /C1 [ %s ]\n", $this->getColorString($endColor))
333            . "  /N 1\n"
334            . " >>\n>>\nshfill\nQ\n";
335    }
336
337    private function getColorSetString(ColorInterface $color) : string
338    {
339        if ($color instanceof Rgb) {
340            return $this->getColorString($color) . ' rgb';
341        }
342
343        if ($color instanceof Cmyk) {
344            return $this->getColorString($color) . ' cmyk';
345        }
346
347        if ($color instanceof Gray) {
348            return $this->getColorString($color) . ' gray';
349        }
350
351        return $this->getColorSetString($color->toCmyk());
352    }
353
354    private function getColorString(ColorInterface $color) : string
355    {
356        if ($color instanceof Rgb) {
357            return sprintf('%s %s %s', $color->getRed() / 255, $color->getGreen() / 255, $color->getBlue() / 255);
358        }
359
360        if ($color instanceof Cmyk) {
361            return sprintf(
362                '%s %s %s %s',
363                $color->getCyan() / 100,
364                $color->getMagenta() / 100,
365                $color->getYellow() / 100,
366                $color->getBlack() / 100
367            );
368        }
369
370        if ($color instanceof Gray) {
371            return sprintf('%s', $color->getGray() / 100);
372        }
373
374        return $this->getColorString($color->toCmyk());
375    }
376}
377