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\Gd;
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\Palette\Color\RGB as RGBColor;
21use Imagine\Image\PointInterface;
22
23/**
24 * Drawer implementation using the GD library
25 */
26final class Drawer implements DrawerInterface
27{
28    /**
29     * @var resource
30     */
31    private $resource;
32
33    /**
34     * @var array
35     */
36    private $info;
37
38    /**
39     * Constructs Drawer with a given gd image resource
40     *
41     * @param resource $resource
42     */
43    public function __construct($resource)
44    {
45        $this->loadGdInfo();
46        $this->resource = $resource;
47    }
48
49    /**
50     * {@inheritdoc}
51     */
52    public function arc(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $thickness = 1)
53    {
54        imagesetthickness($this->resource, max(1, (int) $thickness));
55
56        if (false === imagealphablending($this->resource, true)) {
57            throw new RuntimeException('Draw arc operation failed');
58        }
59
60        if (false === imagearc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color))) {
61            imagealphablending($this->resource, false);
62            throw new RuntimeException('Draw arc operation failed');
63        }
64
65        if (false === imagealphablending($this->resource, false)) {
66            throw new RuntimeException('Draw arc operation failed');
67        }
68
69        return $this;
70    }
71
72    /**
73     * This function does not work properly because of a bug in GD
74     *
75     * {@inheritdoc}
76     */
77    public function chord(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
78    {
79        imagesetthickness($this->resource, max(1, (int) $thickness));
80
81        if ($fill) {
82            $style = IMG_ARC_CHORD;
83        } else {
84            $style = IMG_ARC_CHORD | IMG_ARC_NOFILL;
85        }
86
87        if (false === imagealphablending($this->resource, true)) {
88            throw new RuntimeException('Draw chord operation failed');
89        }
90
91        if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) {
92            imagealphablending($this->resource, false);
93            throw new RuntimeException('Draw chord operation failed');
94        }
95
96        if (false === imagealphablending($this->resource, false)) {
97            throw new RuntimeException('Draw chord operation failed');
98        }
99
100        return $this;
101    }
102
103    /**
104     * {@inheritdoc}
105     */
106    public function ellipse(PointInterface $center, BoxInterface $size, ColorInterface $color, $fill = false, $thickness = 1)
107    {
108        imagesetthickness($this->resource, max(1, (int) $thickness));
109
110        if ($fill) {
111            $callback = 'imagefilledellipse';
112        } else {
113            $callback = 'imageellipse';
114        }
115
116        if (false === imagealphablending($this->resource, true)) {
117            throw new RuntimeException('Draw ellipse operation failed');
118        }
119
120        if (false === $callback($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $this->getColor($color))) {
121            imagealphablending($this->resource, false);
122            throw new RuntimeException('Draw ellipse operation failed');
123        }
124
125        if (false === imagealphablending($this->resource, false)) {
126            throw new RuntimeException('Draw ellipse operation failed');
127        }
128
129        return $this;
130    }
131
132    /**
133     * {@inheritdoc}
134     */
135    public function line(PointInterface $start, PointInterface $end, ColorInterface $color, $thickness = 1)
136    {
137        imagesetthickness($this->resource, max(1, (int) $thickness));
138
139        if (false === imagealphablending($this->resource, true)) {
140            throw new RuntimeException('Draw line operation failed');
141        }
142
143        if (false === imageline($this->resource, $start->getX(), $start->getY(), $end->getX(), $end->getY(), $this->getColor($color))) {
144            imagealphablending($this->resource, false);
145            throw new RuntimeException('Draw line operation failed');
146        }
147
148        if (false === imagealphablending($this->resource, false)) {
149            throw new RuntimeException('Draw line operation failed');
150        }
151
152        return $this;
153    }
154
155    /**
156     * {@inheritdoc}
157     */
158    public function pieSlice(PointInterface $center, BoxInterface $size, $start, $end, ColorInterface $color, $fill = false, $thickness = 1)
159    {
160        imagesetthickness($this->resource, max(1, (int) $thickness));
161
162        if ($fill) {
163            $style = IMG_ARC_EDGED;
164        } else {
165            $style = IMG_ARC_EDGED | IMG_ARC_NOFILL;
166        }
167
168        if (false === imagealphablending($this->resource, true)) {
169            throw new RuntimeException('Draw chord operation failed');
170        }
171
172        if (false === imagefilledarc($this->resource, $center->getX(), $center->getY(), $size->getWidth(), $size->getHeight(), $start, $end, $this->getColor($color), $style)) {
173            imagealphablending($this->resource, false);
174            throw new RuntimeException('Draw chord operation failed');
175        }
176
177        if (false === imagealphablending($this->resource, false)) {
178            throw new RuntimeException('Draw chord operation failed');
179        }
180
181        return $this;
182    }
183
184    /**
185     * {@inheritdoc}
186     */
187    public function dot(PointInterface $position, ColorInterface $color)
188    {
189        if (false === imagealphablending($this->resource, true)) {
190            throw new RuntimeException('Draw point operation failed');
191        }
192
193        if (false === imagesetpixel($this->resource, $position->getX(), $position->getY(), $this->getColor($color))) {
194            imagealphablending($this->resource, false);
195            throw new RuntimeException('Draw point operation failed');
196        }
197
198        if (false === imagealphablending($this->resource, false)) {
199            throw new RuntimeException('Draw point operation failed');
200        }
201
202        return $this;
203    }
204
205    /**
206     * {@inheritdoc}
207     */
208    public function polygon(array $coordinates, ColorInterface $color, $fill = false, $thickness = 1)
209    {
210        imagesetthickness($this->resource, max(1, (int) $thickness));
211
212        if (count($coordinates) < 3) {
213            throw new InvalidArgumentException(sprintf('A polygon must consist of at least 3 points, %d given', count($coordinates)));
214        }
215
216        $points = call_user_func_array('array_merge', array_map(function (PointInterface $p) {
217            return array($p->getX(), $p->getY());
218        }, $coordinates));
219
220        if ($fill) {
221            $callback = 'imagefilledpolygon';
222        } else {
223            $callback = 'imagepolygon';
224        }
225
226        if (false === imagealphablending($this->resource, true)) {
227            throw new RuntimeException('Draw polygon operation failed');
228        }
229
230        if (false === $callback($this->resource, $points, count($coordinates), $this->getColor($color))) {
231            imagealphablending($this->resource, false);
232            throw new RuntimeException('Draw polygon operation failed');
233        }
234
235        if (false === imagealphablending($this->resource, false)) {
236            throw new RuntimeException('Draw polygon operation failed');
237        }
238
239        return $this;
240    }
241
242    /**
243     * {@inheritdoc}
244     */
245    public function text($string, AbstractFont $font, PointInterface $position, $angle = 0, $width = null)
246    {
247        if (!$this->info['FreeType Support']) {
248            throw new RuntimeException('GD is not compiled with FreeType support');
249        }
250
251        $angle    = -1 * $angle;
252        $fontsize = $font->getSize();
253        $fontfile = $font->getFile();
254        $x        = $position->getX();
255        $y        = $position->getY() + $fontsize;
256
257        if ($width !== null) {
258            $string = $this->wrapText($string, $font, $angle, $width);
259        }
260
261        if (false === imagealphablending($this->resource, true)) {
262            throw new RuntimeException('Font mask operation failed');
263        }
264
265        if (false === imagefttext($this->resource, $fontsize, $angle, $x, $y, $this->getColor($font->getColor()), $fontfile, $string)) {
266            imagealphablending($this->resource, false);
267            throw new RuntimeException('Font mask operation failed');
268        }
269
270        if (false === imagealphablending($this->resource, false)) {
271            throw new RuntimeException('Font mask operation failed');
272        }
273
274        return $this;
275    }
276
277    /**
278     * Internal
279     *
280     * Generates a GD color from Color instance
281     *
282     * @param ColorInterface $color
283     *
284     * @return resource
285     *
286     * @throws RuntimeException
287     * @throws InvalidArgumentException
288     */
289    private function getColor(ColorInterface $color)
290    {
291        if (!$color instanceof RGBColor) {
292            throw new InvalidArgumentException('GD driver only supports RGB colors');
293        }
294
295        $gdColor = imagecolorallocatealpha($this->resource, $color->getRed(), $color->getGreen(), $color->getBlue(), (100 - $color->getAlpha()) * 127 / 100);
296        if (false === $gdColor) {
297            throw new RuntimeException(sprintf('Unable to allocate color "RGB(%s, %s, %s)" with transparency of %d percent', $color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha()));
298        }
299
300        return $gdColor;
301    }
302
303    private function loadGdInfo()
304    {
305        if (!function_exists('gd_info')) {
306            throw new RuntimeException('Gd not installed');
307        }
308
309        $this->info = gd_info();
310    }
311
312    /**
313     * Internal
314     *
315     * Fits a string into box with given width
316     */
317    private function wrapText($string, AbstractFont $font, $angle, $width)
318    {
319        $result = '';
320        $words = explode(' ', $string);
321        foreach ($words as $word) {
322            $teststring = $result . ' ' . $word;
323            $testbox = imagettfbbox($font->getSize(), $angle, $font->getFile(), $teststring);
324            if ($testbox[2] > $width) {
325                $result .= ($result == '' ? '' : "\n") . $word;
326            } else {
327                $result .= ($result == '' ? '' : ' ') . $word;
328            }
329        }
330
331        return $result;
332    }
333}
334