1<?php
2
3/*
4 * This file is part of the Imagine package.
5 *
6 * (c) Bulat Shakirzyanov <mallluhuct@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Imagine\Imagick;
13
14use Imagine\Draw\DrawerInterface;
15use Imagine\Exception\InvalidArgumentException;
16use Imagine\Exception\RuntimeException;
17use Imagine\Image\AbstractFont;
18use Imagine\Image\BoxInterface;
19use Imagine\Image\Palette\Color\ColorInterface;
20use Imagine\Image\Point;
21use Imagine\Image\PointInterface;
22
23/**
24 * Drawer implementation using the Imagick PHP extension
25 */
26final class Drawer implements DrawerInterface
27{
28    /**
29     * @var Imagick
30     */
31    private $imagick;
32
33    /**
34     * @param \Imagick $imagick
35     */
36    public function __construct(\Imagick $imagick)
37    {
38        $this->imagick = $imagick;
39    }
40
41    /**
42     * {@inheritdoc}
43     */
44    public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1)
45    {
46        $x      = $center->getX();
47        $y      = $center->getY();
48        $width  = $size->getWidth();
49        $height = $size->getHeight();
50
51        try {
52            $pixel = $this->getColor($color);
53            $arc   = new \ImagickDraw();
54
55            $arc->setStrokeColor($pixel);
56            $arc->setStrokeWidth(max(1, (int) $thickness));
57            $arc->setFillColor('transparent');
58            $arc->arc($x - $width / 2, $y - $height / 2, $x + $width / 2, $y + $height / 2, $start, $end);
59
60            $this->imagick->drawImage($arc);
61
62            $pixel->clear();
63            $pixel->destroy();
64
65            $arc->clear();
66            $arc->destroy();
67        } catch (\ImagickException $e) {
68            throw new RuntimeException('Draw arc operation failed', $e->getCode(), $e);
69        }
70
71        return $this;
72    }
73
74    /**
75     * {@inheritdoc}
76     */
77    public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
78    {
79        $x      = $center->getX();
80        $y      = $center->getY();
81        $width  = $size->getWidth();
82        $height = $size->getHeight();
83
84        try {
85            $pixel = $this->getColor($color);
86            $chord = new \ImagickDraw();
87
88            $chord->setStrokeColor($pixel);
89            $chord->setStrokeWidth(max(1, (int) $thickness));
90
91            if ($fill) {
92                $chord->setFillColor($pixel);
93            } else {
94                $this->line(
95                    new Point(round($x + $width / 2 * cos(deg2rad($start))), round($y + $height / 2 * sin(deg2rad($start)))),
96                    new Point(round($x + $width / 2 * cos(deg2rad($end))), round($y + $height / 2 * sin(deg2rad($end)))),
97                    $color
98                );
99
100                $chord->setFillColor('transparent');
101            }
102
103            $chord->arc(
104                $x - $width / 2,
105                $y - $height / 2,
106                $x + $width / 2,
107                $y + $height / 2,
108                $start,
109                $end
110            );
111
112            $this->imagick->drawImage($chord);
113
114            $pixel->clear();
115            $pixel->destroy();
116
117            $chord->clear();
118            $chord->destroy();
119        } catch (\ImagickException $e) {
120            throw new RuntimeException('Draw chord operation failed', $e->getCode(), $e);
121        }
122
123        return $this;
124    }
125
126    /**
127     * {@inheritdoc}
128     */
129    public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1)
130    {
131        $width  = $size->getWidth();
132        $height = $size->getHeight();
133
134        try {
135            $pixel   = $this->getColor($color);
136            $ellipse = new \ImagickDraw();
137
138            $ellipse->setStrokeColor($pixel);
139            $ellipse->setStrokeWidth(max(1, (int) $thickness));
140
141            if ($fill) {
142                $ellipse->setFillColor($pixel);
143            } else {
144                $ellipse->setFillColor('transparent');
145            }
146
147            $ellipse->ellipse(
148                $center->getX(),
149                $center->getY(),
150                $width / 2,
151                $height / 2,
152                0, 360
153            );
154
155            if (false === $this->imagick->drawImage($ellipse)) {
156                throw new RuntimeException('Ellipse operation failed');
157            }
158
159            $pixel->clear();
160            $pixel->destroy();
161
162            $ellipse->clear();
163            $ellipse->destroy();
164        } catch (\ImagickException $e) {
165            throw new RuntimeException('Draw ellipse operation failed', $e->getCode(), $e);
166        }
167
168        return $this;
169    }
170
171    /**
172     * {@inheritdoc}
173     */
174    public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1)
175    {
176        try {
177            $pixel = $this->getColor($color);
178            $line  = new \ImagickDraw();
179
180            $line->setStrokeColor($pixel);
181            $line->setStrokeWidth(max(1, (int) $thickness));
182            $line->setFillColor($pixel);
183            $line->line(
184                $start->getX(),
185                $start->getY(),
186                $end->getX(),
187                $end->getY()
188            );
189
190            $this->imagick->drawImage($line);
191
192            $pixel->clear();
193            $pixel->destroy();
194
195            $line->clear();
196            $line->destroy();
197        } catch (\ImagickException $e) {
198            throw new RuntimeException('Draw line operation failed', $e->getCode(), $e);
199        }
200
201        return $this;
202    }
203
204    /**
205     * {@inheritdoc}
206     */
207    public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
208    {
209        $width  = $size->getWidth();
210        $height = $size->getHeight();
211
212        $x1 = round($center->getX() + $width / 2 * cos(deg2rad($start)));
213        $y1 = round($center->getY() + $height / 2 * sin(deg2rad($start)));
214        $x2 = round($center->getX() + $width / 2 * cos(deg2rad($end)));
215        $y2 = round($center->getY() + $height / 2 * sin(deg2rad($end)));
216
217        if ($fill) {
218            $this->chord($center, $size, $start, $end, $color, true, $thickness);
219            $this->polygon(
220                array(
221                    $center,
222                    new Point($x1, $y1),
223                    new Point($x2, $y2),
224                ),
225                $color,
226                true,
227                $thickness
228            );
229        } else {
230            $this->arc($center, $size, $start, $end, $color, $thickness);
231            $this->line($center, new Point($x1, $y1), $color, $thickness);
232            $this->line($center, new Point($x2, $y2), $color, $thickness);
233        }
234
235        return $this;
236    }
237
238    /**
239     * {@inheritdoc}
240     */
241    public function dot(PointInterface $position, ColorInterface $color)
242    {
243        $x = $position->getX();
244        $y = $position->getY();
245
246        try {
247            $pixel = $this->getColor($color);
248            $point = new \ImagickDraw();
249
250            $point->setFillColor($pixel);
251            $point->point($x, $y);
252
253            $this->imagick->drawimage($point);
254
255            $pixel->clear();
256            $pixel->destroy();
257
258            $point->clear();
259            $point->destroy();
260        } catch (\ImagickException $e) {
261            throw new RuntimeException('Draw point operation failed', $e->getCode(), $e);
262        }
263
264        return $this;
265    }
266
267    /**
268     * {@inheritdoc}
269     */
270    public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1)
271    {
272        if (count($coordinates) < 3) {
273            throw new InvalidArgumentException(sprintf('Polygon must consist of at least 3 coordinates, %d given', count($coordinates)));
274        }
275
276        $points = array_map(function (PointInterface $p) {
277            return array('x' => $p->getX(), 'y' => $p->getY());
278        }, $coordinates);
279
280        try {
281            $pixel   = $this->getColor($color);
282            $polygon = new \ImagickDraw();
283
284            $polygon->setStrokeColor($pixel);
285            $polygon->setStrokeWidth(max(1, (int) $thickness));
286
287            if ($fill) {
288                $polygon->setFillColor($pixel);
289            } else {
290                $polygon->setFillColor('transparent');
291            }
292
293            $polygon->polygon($points);
294            $this->imagick->drawImage($polygon);
295
296            $pixel->clear();
297            $pixel->destroy();
298
299            $polygon->clear();
300            $polygon->destroy();
301        } catch (\ImagickException $e) {
302            throw new RuntimeException('Draw polygon operation failed', $e->getCode(), $e);
303        }
304
305        return $this;
306    }
307
308    /**
309     * {@inheritdoc}
310     */
311    public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null)
312    {
313        try {
314            $pixel = $this->getColor($font->getColor());
315            $text  = new \ImagickDraw();
316
317            $text->setFont($font->getFile());
318            /**
319             * @see http://www.php.net/manual/en/imagick.queryfontmetrics.php#101027
320             *
321             * ensure font resolution is the same as GD's hard-coded 96
322             */
323            if (version_compare(phpversion("imagick"), "3.0.2", ">=")) {
324                $text->setResolution(96, 96);
325                $text->setFontSize($font->getSize());
326            } else {
327                $text->setFontSize((int) ($font->getSize() * (96 / 72)));
328            }
329            $text->setFillColor($pixel);
330            $text->setTextAntialias(true);
331
332            $info = $this->imagick->queryFontMetrics($text, $string);
333            $rad  = deg2rad($angle);
334            $cos  = cos($rad);
335            $sin  = sin($rad);
336
337            // round(0 * $cos - 0 * $sin)
338            $x1 = 0;
339            $x2 = round($info['characterWidth'] * $cos - $info['characterHeight'] * $sin);
340            // round(0 * $sin + 0 * $cos)
341            $y1 = 0;
342            $y2 = round($info['characterWidth'] * $sin + $info['characterHeight'] * $cos);
343
344            $xdiff = 0 - min($x1, $x2);
345            $ydiff = 0 - min($y1, $y2);
346
347            if ($width !== null) {
348                $string = $this->wrapText($string, $text, $angle, $width);
349            }
350
351            $this->imagick->annotateImage(
352                $text, $position->getX() + $x1 + $xdiff,
353                $position->getY() + $y2 + $ydiff, $angle, $string
354            );
355
356            $pixel->clear();
357            $pixel->destroy();
358
359            $text->clear();
360            $text->destroy();
361        } catch (\ImagickException $e) {
362            throw new RuntimeException('Draw text operation failed', $e->getCode(), $e);
363        }
364
365        return $this;
366    }
367
368    /**
369     * Gets specifically formatted color string from ColorInterface instance
370     *
371     * @param ColorInterface $color
372     *
373     * @return string
374     */
375    private function getColor(ColorInterface $color)
376    {
377        $pixel = new \ImagickPixel((string) $color);
378        $pixel->setColorValue(\Imagick::COLOR_ALPHA, $color->getAlpha() / 100);
379
380        return $pixel;
381    }
382
383    /**
384     * Internal
385     *
386     * Fits a string into box with given width
387     */
388    private function wrapText($string, $text, $angle, $width)
389    {
390        $result = '';
391        $words = explode(' ', $string);
392        foreach ($words as $word) {
393            $teststring = $result . ' ' . $word;
394            $testbox = $this->imagick->queryFontMetrics($text, $teststring, true);
395            if ($testbox['textWidth'] > $width) {
396                $result .= ($result == '' ? '' : "\n") . $word;
397            } else {
398                $result .= ($result == '' ? '' : ' ') . $word;
399            }
400        }
401
402        return $result;
403    }
404}
405