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