| Total Complexity | 81 |
| Total Lines | 603 |
| Duplicated Lines | 0 % |
| Changes | 2 | ||
| Bugs | 0 | Features | 0 |
Complex classes like ImageMagick often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ImageMagick, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 31 | class ImageMagick extends AbstractManipulator |
||
| 32 | { |
||
| 33 | /** @var Imagick */ |
||
| 34 | protected $_image; |
||
| 35 | |||
| 36 | /** |
||
| 37 | * ImageMagick constructor. |
||
| 38 | * |
||
| 39 | * @param string $image |
||
| 40 | */ |
||
| 41 | public function __construct($image) |
||
| 42 | { |
||
| 43 | $this->_fileName = $image; |
||
| 44 | |||
| 45 | try |
||
| 46 | { |
||
| 47 | $this->memoryCheck(); |
||
| 48 | } |
||
| 49 | catch (Exception) |
||
| 50 | { |
||
| 51 | // Just pass through |
||
| 52 | } |
||
| 53 | } |
||
| 54 | |||
| 55 | /** |
||
| 56 | * Checks whether the ImageMagick class is present. |
||
| 57 | * |
||
| 58 | * @return bool Whether the ImageMagick extension is available. |
||
| 59 | */ |
||
| 60 | public static function canUse() |
||
| 63 | } |
||
| 64 | |||
| 65 | /** |
||
| 66 | * Loads an image file into the image engine for processing |
||
| 67 | * |
||
| 68 | * @return bool |
||
| 69 | */ |
||
| 70 | public function createImageFromFile() |
||
| 98 | } |
||
| 99 | |||
| 100 | /** |
||
| 101 | * Sets the image sizes. |
||
| 102 | */ |
||
| 103 | protected function _setImage(): void |
||
| 104 | { |
||
| 105 | // Update the image size values |
||
| 106 | $this->_image->setFirstIterator(); |
||
| 107 | |||
| 108 | try |
||
| 109 | { |
||
| 110 | $this->_width = $this->imageDimensions[0] = $this->_image->getImageWidth(); |
||
| 111 | $this->_height = $this->imageDimensions[1] = $this->_image->getImageHeight(); |
||
| 112 | } |
||
| 113 | catch (ImagickException) |
||
| 114 | { |
||
| 115 | $this->_width = $this->imageDimensions[0] ?? 0; |
||
| 116 | $this->_height = $this->imageDimensions[1] ?? 0; |
||
| 117 | } |
||
| 118 | } |
||
| 119 | |||
| 120 | /** |
||
| 121 | * Loads an image from a web address into the image engine for processing |
||
| 122 | * |
||
| 123 | * @return bool |
||
| 124 | */ |
||
| 125 | public function createImageFromWeb() |
||
| 155 | } |
||
| 156 | |||
| 157 | /** |
||
| 158 | * Resize an image proportionally to fit within the defined max_width and max_height limits |
||
| 159 | * |
||
| 160 | * What it does: |
||
| 161 | * |
||
| 162 | * - Will do nothing to the image if the file fits within the size limits |
||
| 163 | * |
||
| 164 | * @param int|null $max_width The maximum allowed width |
||
| 165 | * @param int|null $max_height The maximum allowed height |
||
| 166 | * @param bool $strip Whether to have IM strip EXIF data as GD will |
||
| 167 | * @param bool $force_resize = false Whether to override defaults and resize it |
||
| 168 | * @param bool $thumbnail True if creating a simple thumbnail |
||
| 169 | * |
||
| 170 | * @return bool Whether resize was successful. |
||
| 171 | */ |
||
| 172 | public function resizeImage($max_width = null, $max_height = null, $strip = false, $force_resize = false, $thumbnail = false) |
||
| 173 | { |
||
| 174 | $success = true; |
||
| 175 | |||
| 176 | // No image, no further |
||
| 177 | if (empty($this->_image)) |
||
| 178 | { |
||
| 179 | return false; |
||
| 180 | } |
||
| 181 | |||
| 182 | // Set the input and output image size |
||
| 183 | $src_width = $this->_width; |
||
| 184 | $src_height = $this->_height; |
||
| 185 | |||
| 186 | // Allow for a re-encode to the same size |
||
| 187 | $max_width = $max_width ?? $src_width; |
||
| 188 | $max_height = $max_height ?? $src_height; |
||
| 189 | |||
| 190 | // Determine whether to resize to max width or to max height (depending on the limits.) |
||
| 191 | [$dst_width, $dst_height] = $this->imageRatio($max_width, $max_height); |
||
| 192 | |||
| 193 | // Don't bother resizing if it's already smaller... |
||
| 194 | if (!empty($dst_width) && !empty($dst_height) && ($dst_width < $src_width || $dst_height < $src_height || $force_resize)) |
||
| 195 | { |
||
| 196 | try |
||
| 197 | { |
||
| 198 | if ($thumbnail) |
||
| 199 | { |
||
| 200 | $success = $this->_image->thumbnailImage($dst_width, $dst_height, true); |
||
| 201 | } |
||
| 202 | elseif ($this->_image->getNumberImages() > 1) |
||
| 203 | { |
||
| 204 | // Animated GIFs are a special case, they need to be resized individually |
||
| 205 | $success = $this->resizeGifImage($dst_width, $dst_height); |
||
| 206 | } |
||
| 207 | else |
||
| 208 | { |
||
| 209 | $success = $this->_image->resizeImage($dst_width, $dst_height, Imagick::FILTER_LANCZOS, .9891, true); |
||
| 210 | } |
||
| 211 | } |
||
| 212 | catch (ImagickException) |
||
| 213 | { |
||
| 214 | return false; |
||
| 215 | } |
||
| 216 | |||
| 217 | $this->_setImage(); |
||
| 218 | } |
||
| 219 | |||
| 220 | // Remove EXIF / ICC data? |
||
| 221 | if ($strip) |
||
| 222 | { |
||
| 223 | try |
||
| 224 | { |
||
| 225 | $success = $this->_image->stripImage() && $success; |
||
| 226 | } |
||
| 227 | catch (ImagickException) |
||
| 228 | { |
||
| 229 | return $success; |
||
| 230 | } |
||
| 231 | |||
| 232 | } |
||
| 233 | |||
| 234 | return $success; |
||
| 235 | } |
||
| 236 | |||
| 237 | /** |
||
| 238 | * Resizes a GIF image to the specified dimensions, adjusting each frame individually. |
||
| 239 | * |
||
| 240 | * @param int $dst_width The desired width of the resized image. |
||
| 241 | * @param int $dst_height The desired height of the resized image. |
||
| 242 | * @return bool Indicates whether the resizing operation was successful for all frames. |
||
| 243 | */ |
||
| 244 | public function resizeGifImage($dst_width, $dst_height) |
||
| 245 | { |
||
| 246 | $success = true; |
||
| 247 | |||
| 248 | // Explode the gif so each frame is a full image |
||
| 249 | $this->_image = $this->_image->coalesceImages(); |
||
| 250 | |||
| 251 | // Resize every frame individually |
||
| 252 | foreach ($this->_image as $frame) |
||
| 253 | { |
||
| 254 | $success .= $frame->resizeImage($dst_width, $dst_height, \Imagick::FILTER_LANCZOS, .9891, true); |
||
| 255 | } |
||
| 256 | |||
| 257 | $this->_image = $this->_image->deconstructImages(); |
||
| 258 | |||
| 259 | return $success; |
||
| 260 | } |
||
| 261 | |||
| 262 | /** |
||
| 263 | * Output the image resource to a file in a chosen format |
||
| 264 | * |
||
| 265 | * @param string $file_name where to save the image, if '' output to screen |
||
| 266 | * @param int $preferred_format jpg,png,gif, etc |
||
| 267 | * @param int $quality the jpg image quality |
||
| 268 | * |
||
| 269 | * @return bool |
||
| 270 | */ |
||
| 271 | public function output($file_name = '', $preferred_format = IMAGETYPE_JPEG, $quality = 85) |
||
| 272 | { |
||
| 273 | // An unknown type |
||
| 274 | if (!isset(Image::DEFAULT_FORMATS[$preferred_format])) |
||
| 275 | { |
||
| 276 | return false; |
||
| 277 | } |
||
| 278 | |||
| 279 | switch ($preferred_format) |
||
| 280 | { |
||
| 281 | case IMAGETYPE_GIF: |
||
| 282 | $success = $this->_image->setImageFormat('gif'); |
||
| 283 | break; |
||
| 284 | case IMAGETYPE_PNG: |
||
| 285 | // Save a few bytes the only way, realistically, we can |
||
| 286 | $this->_image->setOption('png:compression-level', '9'); |
||
| 287 | $this->_image->setOption('png:exclude-chunk', 'all'); |
||
| 288 | $success = $this->_image->setImageFormat('png'); |
||
| 289 | break; |
||
| 290 | case IMAGETYPE_WBMP: |
||
| 291 | $success = $this->_image->setImageFormat('wbmp'); |
||
| 292 | break; |
||
| 293 | case IMAGETYPE_BMP: |
||
| 294 | $success = $this->_image->setImageFormat('bmp'); |
||
| 295 | break; |
||
| 296 | case IMAGETYPE_WEBP: |
||
| 297 | $this->_image->setImageCompressionQuality($quality); |
||
| 298 | $success = $this->_image->setImageFormat('webp'); |
||
| 299 | break; |
||
| 300 | default: |
||
| 301 | $this->_image->borderImage('white', 0, 0); |
||
| 302 | $this->_image->setImageCompression(Imagick::COMPRESSION_JPEG); |
||
| 303 | $this->_image->setImageCompressionQuality($quality); |
||
| 304 | $success = $this->_image->setImageFormat('jpeg'); |
||
| 305 | break; |
||
| 306 | } |
||
| 307 | |||
| 308 | try |
||
| 309 | { |
||
| 310 | if ($success) |
||
| 311 | { |
||
| 312 | // Screen or file, your choice |
||
| 313 | if (empty($file_name)) |
||
| 314 | { |
||
| 315 | echo $this->_image->getImagesBlob(); |
||
| 316 | } |
||
| 317 | elseif ($preferred_format === IMAGETYPE_GIF && $this->_image->getNumberImages() !== 0) |
||
| 318 | { |
||
| 319 | // Write all animated gif frames |
||
| 320 | $success = $this->_image->writeImages($file_name, true); |
||
| 321 | } |
||
| 322 | else |
||
| 323 | { |
||
| 324 | $success = $this->_image->writeImage($file_name); |
||
| 325 | } |
||
| 326 | } |
||
| 327 | } |
||
| 328 | catch (Exception) |
||
| 329 | { |
||
| 330 | return false; |
||
| 331 | } |
||
| 332 | |||
| 333 | // Update the sizes array to the output file |
||
| 334 | if ($success && $file_name !== '') |
||
| 335 | { |
||
| 336 | $this->_fileName = $file_name; |
||
| 337 | $this->imageDimensions[2] = $preferred_format; |
||
| 338 | $this->_setImage(); |
||
| 339 | } |
||
| 340 | |||
| 341 | return $success; |
||
| 342 | } |
||
| 343 | |||
| 344 | /** |
||
| 345 | * Autorotate an image based on its EXIF Orientation tag. |
||
| 346 | * |
||
| 347 | * What it does: |
||
| 348 | * |
||
| 349 | * - Checks exif data for orientation flag and rotates image so its proper |
||
| 350 | * - Updates orientation flag if rotation was required |
||
| 351 | * |
||
| 352 | * @return bool |
||
| 353 | */ |
||
| 354 | public function autoRotate() |
||
| 412 | } |
||
| 413 | |||
| 414 | /** |
||
| 415 | * Returns the ORIENTATION constant for an image |
||
| 416 | * |
||
| 417 | * @return int |
||
| 418 | */ |
||
| 419 | public function getOrientation() |
||
| 420 | { |
||
| 421 | try |
||
| 422 | { |
||
| 423 | $this->orientation = $this->_image->getImageOrientation(); |
||
| 424 | } |
||
| 425 | catch (ImagickException) |
||
| 426 | { |
||
| 427 | $this->orientation = 0; |
||
| 428 | } |
||
| 429 | |||
| 430 | return $this->orientation; |
||
| 431 | } |
||
| 432 | |||
| 433 | /** |
||
| 434 | * Returns if the image has any alpha pixels. |
||
| 435 | * |
||
| 436 | * @return bool |
||
| 437 | */ |
||
| 438 | public function getTransparency() |
||
| 439 | { |
||
| 440 | // No image, return false |
||
| 441 | if (empty($this->_image)) |
||
| 442 | { |
||
| 443 | return false; |
||
| 444 | } |
||
| 445 | |||
| 446 | $checkImage = clone $this->_image; |
||
| 447 | |||
| 448 | try |
||
| 449 | { |
||
| 450 | // Scale down large images to reduce processing time |
||
| 451 | if ($this->_width > 1024 || $this->_height > 1024) |
||
| 452 | { |
||
| 453 | // This is only used to look for transparency, it is not intended to be a quality image. |
||
| 454 | $scaleValue = $this->imageScaleFactor(800); |
||
| 455 | $checkImage->scaleImage($scaleValue[0], $scaleValue[1], true); |
||
| 456 | } |
||
| 457 | } |
||
| 458 | catch (ImagickException) |
||
| 459 | { |
||
| 460 | $checkImage->clear(); |
||
| 461 | |||
| 462 | return true; |
||
| 463 | } |
||
| 464 | |||
| 465 | // First attempt by looking at the channel statistics (faster) |
||
| 466 | $transparent = $this->checkOpacityChannel($checkImage); |
||
| 467 | if ($transparent !== null) |
||
| 468 | { |
||
| 469 | // Failing channel stats?, resort to pixel inspection |
||
| 470 | $transparent = $this->checkOpacityPixelInspection($checkImage); |
||
| 471 | } |
||
| 472 | |||
| 473 | $checkImage->clear(); |
||
| 474 | |||
| 475 | return $transparent; |
||
| 476 | } |
||
| 477 | |||
| 478 | /** |
||
| 479 | * Does pixel by pixel inspection to determine if any have an alpha value < 1 |
||
| 480 | * |
||
| 481 | * - Any pixel alpha < 1 is not perfectly opaque. |
||
| 482 | * - Resizes images > 1024x1024 to reduce pixel count |
||
| 483 | * - Used as a backup function should checkOpacityChannel() fail |
||
| 484 | * |
||
| 485 | * @param Imagick $checkImage |
||
| 486 | * @return bool |
||
| 487 | */ |
||
| 488 | public function checkOpacityPixelInspection($checkImage): bool |
||
| 489 | { |
||
| 490 | $checkImage = $checkImage ?? clone $this->_image; |
||
| 491 | |||
| 492 | try |
||
| 493 | { |
||
| 494 | $transparency = false; |
||
| 495 | |||
| 496 | $pixel_iterator = $checkImage->getPixelIterator(); |
||
| 497 | |||
| 498 | // Look at each one, or until we find the first alpha pixel |
||
| 499 | foreach ($pixel_iterator as $pixels) |
||
| 500 | { |
||
| 501 | foreach ($pixels as $pixel) |
||
| 502 | { |
||
| 503 | $color = $pixel->getColor(); |
||
| 504 | if ($color['a'] < 1) |
||
| 505 | { |
||
| 506 | $transparency = true; |
||
| 507 | break 2; |
||
| 508 | } |
||
| 509 | } |
||
| 510 | } |
||
| 511 | } |
||
| 512 | catch (ImagickException) |
||
| 513 | { |
||
| 514 | // We don't know what it is, so don't mess with it |
||
| 515 | return true; |
||
| 516 | } |
||
| 517 | |||
| 518 | return $transparency; |
||
| 519 | } |
||
| 520 | |||
| 521 | /** |
||
| 522 | * Attempts to use imagick getImageChannelMean to determine alpha/opacity channel statistics |
||
| 523 | * |
||
| 524 | * - An opaque image will have 0 standard deviation and a mean of 1 (65535) |
||
| 525 | * - If failure returns null, otherwise bool |
||
| 526 | * |
||
| 527 | * @param Imagick $checkImage |
||
| 528 | * @return bool|null |
||
| 529 | */ |
||
| 530 | public function checkOpacityChannel($checkImage): ?bool |
||
| 531 | { |
||
| 532 | $checkImage = $checkImage ?? clone $this->_image; |
||
| 533 | |||
| 534 | try |
||
| 535 | { |
||
| 536 | $transparent = true; |
||
| 537 | $stats = $checkImage->getImageChannelMean(Imagick::CHANNEL_OPACITY); |
||
| 538 | |||
| 539 | // If mean = 65535 and std = 0, then its perfectly opaque. |
||
| 540 | $mean = (int) $stats['mean']; |
||
| 541 | if (($mean === 65535 || $mean === 0) && (int) $stats['standardDeviation'] === 0) |
||
| 542 | { |
||
| 543 | $transparent = false; |
||
| 544 | } |
||
| 545 | } |
||
| 546 | catch (ImagickException) |
||
| 547 | { |
||
| 548 | $transparent = null; |
||
| 549 | } |
||
| 550 | |||
| 551 | return $transparent; |
||
| 552 | } |
||
| 553 | |||
| 554 | /** |
||
| 555 | * Function to generate an image containing some text. |
||
| 556 | * Attempts to adjust font size to fit within bounds |
||
| 557 | * |
||
| 558 | * @param string $text The text the image should contain |
||
| 559 | * @param int $width Width of the final image |
||
| 560 | * @param int $height Height of the image |
||
| 561 | * @param string $format Type of the image (valid types are png, jpeg, gif) |
||
| 562 | * |
||
| 563 | * @return bool|string The image or false on error |
||
| 564 | */ |
||
| 565 | public function generateTextImage($text, $width = 100, $height = 75, $format = 'png') |
||
| 603 | } |
||
| 604 | } |
||
| 605 | |||
| 606 | /** |
||
| 607 | * Check if this installation supports webP |
||
| 608 | * |
||
| 609 | * @return bool |
||
| 610 | */ |
||
| 611 | public function hasWebpSupport(): bool |
||
| 616 | } |
||
| 617 | |||
| 618 | /** |
||
| 619 | * CLean up |
||
| 620 | */ |
||
| 621 | public function __destruct() |
||
| 634 | } |
||
| 635 | } |
||
| 636 |