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