| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * WordPress Imagick Image Editor 4 * 5 * @package WordPress 6 * @subpackage Image_Editor 7 */ 8 9 /** 10 * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module 11 * 12 * @since 3.5.0 13 * 14 * @see WP_Image_Editor 15 */ 16 class WP_Image_Editor_Imagick extends WP_Image_Editor { 17 /** 18 * Imagick object. 19 * 20 * @var Imagick 21 */ 22 protected $image; 23 24 public function __destruct() { 25 if ( $this->image instanceof Imagick ) { 26 // We don't need the original in memory anymore. 27 $this->image->clear(); 28 $this->image->destroy(); 29 } 30 } 31 32 /** 33 * Checks to see if current environment supports Imagick. 34 * 35 * We require Imagick 2.2.0 or greater, based on whether the queryFormats() 36 * method can be called statically. 37 * 38 * @since 3.5.0 39 * 40 * @param array $args 41 * @return bool 42 */ 43 public static function test( $args = array() ) { 44 45 // First, test Imagick's extension and classes. 46 if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) { 47 return false; 48 } 49 50 if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) { 51 return false; 52 } 53 54 $required_methods = array( 55 'clear', 56 'destroy', 57 'valid', 58 'getimage', 59 'writeimage', 60 'getimageblob', 61 'getimagegeometry', 62 'getimageformat', 63 'setimageformat', 64 'setimagecompression', 65 'setimagecompressionquality', 66 'setimagepage', 67 'setoption', 68 'scaleimage', 69 'cropimage', 70 'rotateimage', 71 'flipimage', 72 'flopimage', 73 'readimage', 74 'readimageblob', 75 ); 76 77 // Now, test for deep requirements within Imagick. 78 if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) { 79 return false; 80 } 81 82 $class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) ); 83 if ( array_diff( $required_methods, $class_methods ) ) { 84 return false; 85 } 86 87 return true; 88 } 89 90 /** 91 * Checks to see if editor supports the mime-type specified. 92 * 93 * @since 3.5.0 94 * 95 * @param string $mime_type 96 * @return bool 97 */ 98 public static function supports_mime_type( $mime_type ) { 99 $imagick_extension = strtoupper( self::get_extension( $mime_type ) ); 100 101 if ( ! $imagick_extension ) { 102 return false; 103 } 104 105 /* 106 * setIteratorIndex is optional unless mime is an animated format. 107 * Here, we just say no if you are missing it and aren't loading a jpeg. 108 */ 109 if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) { 110 return false; 111 } 112 113 try { 114 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 115 return ( (bool) @Imagick::queryFormats( $imagick_extension ) ); 116 } catch ( Exception $e ) { 117 return false; 118 } 119 } 120 121 /** 122 * Loads image from $this->file into new Imagick Object. 123 * 124 * @since 3.5.0 125 * 126 * @return true|WP_Error True if loaded; WP_Error on failure. 127 */ 128 public function load() { 129 if ( $this->image instanceof Imagick ) { 130 return true; 131 } 132 133 if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) { 134 return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file ); 135 } 136 137 /* 138 * Even though Imagick uses less PHP memory than GD, set higher limit 139 * for users that have low PHP.ini limits. 140 */ 141 wp_raise_memory_limit( 'image' ); 142 143 try { 144 $this->image = new Imagick(); 145 $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); 146 147 if ( 'pdf' === $file_extension ) { 148 $pdf_loaded = $this->pdf_load_source(); 149 150 if ( is_wp_error( $pdf_loaded ) ) { 151 return $pdf_loaded; 152 } 153 } else { 154 if ( wp_is_stream( $this->file ) ) { 155 // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead. 156 $this->image->readImageBlob( file_get_contents( $this->file ), $this->file ); 157 } else { 158 $this->image->readImage( $this->file ); 159 } 160 } 161 162 if ( ! $this->image->valid() ) { 163 return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); 164 } 165 166 // Select the first frame to handle animated images properly. 167 if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { 168 $this->image->setIteratorIndex( 0 ); 169 } 170 171 if ( 'pdf' === $file_extension ) { 172 $this->remove_pdf_alpha_channel(); 173 } 174 175 $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); 176 } catch ( Exception $e ) { 177 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); 178 } 179 180 $updated_size = $this->update_size(); 181 182 if ( is_wp_error( $updated_size ) ) { 183 return $updated_size; 184 } 185 186 return $this->set_quality(); 187 } 188 189 /** 190 * Sets Image Compression quality on a 1-100% scale. 191 * 192 * @since 3.5.0 193 * @since 6.8.0 The `$dims` parameter was added. 194 * 195 * @param int $quality Compression Quality. Range: [1,100] 196 * @param array $dims Optional. Image dimensions array with 'width' and 'height' keys. 197 * @return true|WP_Error True if set successfully; WP_Error on failure. 198 */ 199 public function set_quality( $quality = null, $dims = array() ) { 200 $quality_result = parent::set_quality( $quality, $dims ); 201 if ( is_wp_error( $quality_result ) ) { 202 return $quality_result; 203 } else { 204 $quality = $this->get_quality(); 205 } 206 207 try { 208 switch ( $this->mime_type ) { 209 case 'image/jpeg': 210 $this->image->setImageCompressionQuality( $quality ); 211 $this->image->setCompressionQuality( $quality ); 212 $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); 213 break; 214 case 'image/webp': 215 $webp_info = wp_get_webp_info( $this->file ); 216 217 if ( 'lossless' === $webp_info['type'] ) { 218 // Use WebP lossless settings. 219 $this->image->setImageCompressionQuality( 100 ); 220 $this->image->setCompressionQuality( 100 ); 221 $this->image->setOption( 'webp:lossless', 'true' ); 222 parent::set_quality( 100 ); 223 } else { 224 $this->image->setImageCompressionQuality( $quality ); 225 $this->image->setCompressionQuality( $quality ); 226 } 227 break; 228 case 'image/avif': 229 // Set the AVIF encoder to work faster, with minimal impact on image size. 230 $this->image->setOption( 'heic:speed', 7 ); 231 $this->image->setImageCompressionQuality( $quality ); 232 $this->image->setCompressionQuality( $quality ); 233 break; 234 default: 235 $this->image->setImageCompressionQuality( $quality ); 236 $this->image->setCompressionQuality( $quality ); 237 } 238 } catch ( Exception $e ) { 239 return new WP_Error( 'image_quality_error', $e->getMessage() ); 240 } 241 return true; 242 } 243 244 245 /** 246 * Sets or updates current image size. 247 * 248 * @since 3.5.0 249 * 250 * @param int|null $width Image width. 251 * @param int|null $height Image height. 252 * @return true|WP_Error 253 */ 254 protected function update_size( $width = null, $height = null ) { 255 $size = null; 256 257 if ( ! $width || ! $height ) { 258 try { 259 $size = $this->image->getImageGeometry(); 260 } catch ( Exception $e ) { 261 return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file ); 262 } 263 } 264 265 if ( ! $width ) { 266 $width = $size['width']; 267 } 268 269 if ( ! $height ) { 270 $height = $size['height']; 271 } 272 273 /* 274 * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images 275 * are properly sized without affecting previous `getImageGeometry` behavior. 276 */ 277 if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) { 278 $size = wp_getimagesize( $this->file ); 279 $width = $size[0]; 280 $height = $size[1]; 281 } 282 283 return parent::update_size( $width, $height ); 284 } 285 286 /** 287 * Sets Imagick time limit. 288 * 289 * Depending on configuration, Imagick processing may take time. 290 * 291 * Multiple problems exist if PHP times out before ImageMagick completed: 292 * 1. Temporary files aren't cleaned by ImageMagick garbage collection. 293 * 2. No clear error is provided. 294 * 3. The cause of such timeout can be hard to pinpoint. 295 * 296 * This function, which is expected to be run before heavy image routines, resolves 297 * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set. 298 * 299 * However seems it introduces more problems than it fixes, 300 * see https://core.trac.wordpress.org/ticket/58202. 301 * 302 * Note: 303 * - Imagick resource exhaustion does not issue catchable exceptions (yet). 304 * See https://github.com/Imagick/imagick/issues/333. 305 * - The resource limit is not saved/restored. It applies to subsequent 306 * image operations within the time of the HTTP request. 307 * 308 * @since 6.2.0 309 * @deprecated 6.3.0 No longer used in core. 310 * 311 * @return int|null The new limit on success, null on failure. 312 */ 313 public static function set_imagick_time_limit() { 314 _deprecated_function( __METHOD__, '6.3.0' ); 315 316 if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) { 317 return null; 318 } 319 320 // Returns PHP_FLOAT_MAX if unset. 321 $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME ); 322 323 // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX. 324 $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout; 325 326 $php_timeout = (int) ini_get( 'max_execution_time' ); 327 328 if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) { 329 $limit = (float) 0.8 * $php_timeout; 330 Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit ); 331 332 return $limit; 333 } 334 } 335 336 /** 337 * Resizes current image. 338 * 339 * At minimum, either a height or width must be provided. 340 * If one of the two is set to null, the resize will 341 * maintain aspect ratio according to the provided dimension. 342 * 343 * @since 3.5.0 344 * 345 * @param int|null $max_w Image width. 346 * @param int|null $max_h Image height. 347 * @param bool|array $crop { 348 * Optional. Image cropping behavior. If false, the image will be scaled (default). 349 * If true, image will be cropped to the specified dimensions using center positions. 350 * If an array, the image will be cropped using the array to specify the crop location: 351 * 352 * @type string $0 The x crop position. Accepts 'left', 'center', or 'right'. 353 * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. 354 * } 355 * @return true|WP_Error 356 */ 357 public function resize( $max_w, $max_h, $crop = false ) { 358 if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) { 359 return true; 360 } 361 362 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); 363 if ( ! $dims ) { 364 return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) ); 365 } 366 367 list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; 368 369 if ( $crop ) { 370 return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); 371 } 372 373 $this->set_quality( 374 null, 375 array( 376 'width' => $dst_w, 377 'height' => $dst_h, 378 ) 379 ); 380 381 // Execute the resize. 382 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 383 if ( is_wp_error( $thumb_result ) ) { 384 return $thumb_result; 385 } 386 387 return $this->update_size( $dst_w, $dst_h ); 388 } 389 390 /** 391 * Efficiently resize the current image 392 * 393 * This is a WordPress specific implementation of Imagick::thumbnailImage(), 394 * which resizes an image to given dimensions and removes any associated profiles. 395 * 396 * @since 4.5.0 397 * 398 * @param int $dst_w The destination width. 399 * @param int $dst_h The destination height. 400 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. 401 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. 402 * @return void|WP_Error 403 */ 404 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { 405 $allowed_filters = array( 406 'FILTER_POINT', 407 'FILTER_BOX', 408 'FILTER_TRIANGLE', 409 'FILTER_HERMITE', 410 'FILTER_HANNING', 411 'FILTER_HAMMING', 412 'FILTER_BLACKMAN', 413 'FILTER_GAUSSIAN', 414 'FILTER_QUADRATIC', 415 'FILTER_CUBIC', 416 'FILTER_CATROM', 417 'FILTER_MITCHELL', 418 'FILTER_LANCZOS', 419 'FILTER_BESSEL', 420 'FILTER_SINC', 421 ); 422 423 /** 424 * Set the filter value if '$filter_name' name is in the allowed list and the related 425 * Imagick constant is defined or fall back to the default filter. 426 */ 427 if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) { 428 $filter = constant( 'Imagick::' . $filter_name ); 429 } else { 430 $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false; 431 } 432 433 /** 434 * Filters whether to strip metadata from images when they're resized. 435 * 436 * This filter only applies when resizing using the Imagick editor since GD 437 * always strips profiles by default. 438 * 439 * @since 4.5.0 440 * 441 * @param bool $strip_meta Whether to strip image metadata during resizing. Default true. 442 */ 443 if ( apply_filters( 'image_strip_meta', $strip_meta ) ) { 444 $this->strip_meta(); // Fail silently if not supported. 445 } 446 447 try { 448 /** 449 * Special handling for certain types of PNG images: 450 * 1. For PNG images, we need to specify compression settings and remove unneeded chunks. 451 * 2. For indexed PNG images, the number of colors must not exceed 256. 452 * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved. 453 * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving 454 * the image using ImageMagick's 'png8' format, because that supports only binary (1 bit) transparency. 455 * 456 * For #4 we want to check whether the image has a 1-bit alpha channel before resizing, because resizing 457 * may cause the number of alpha values to multiply due to antialiasing. If the original image had only a 458 * 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images. 459 * 460 * Perform all the necessary checks before resizing the image and store the results in variables for later use. 461 */ 462 $is_png = false; 463 $is_indexed_png = false; 464 $is_indexed_png_with_alpha_channel = false; 465 $is_indexed_png_with_true_alpha_transparency = false; 466 467 if ( 'image/png' === $this->mime_type ) { 468 $is_png = true; 469 470 if ( 471 is_callable( array( $this->image, 'getImageProperty' ) ) 472 && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' ) 473 ) { 474 $is_indexed_png = true; 475 476 if ( 477 is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 478 && $this->image->getImageAlphaChannel() 479 ) { 480 $is_indexed_png_with_alpha_channel = true; 481 482 if ( 483 is_callable( array( $this->image, 'getImageChannelDepth' ) ) 484 && defined( 'Imagick::CHANNEL_ALPHA' ) 485 && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA ) 486 ) { 487 $is_indexed_png_with_true_alpha_transparency = true; 488 } 489 } 490 } 491 } 492 493 /* 494 * To be more efficient, resample large images to 5x the destination size before resizing 495 * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111), 496 * unless we would be resampling to a scale smaller than 128x128. 497 */ 498 if ( is_callable( array( $this->image, 'sampleImage' ) ) ) { 499 $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] ); 500 $sample_factor = 5; 501 502 if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) { 503 $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor ); 504 } 505 } 506 507 /* 508 * Use resizeImage() when it's available and a valid filter value is set. 509 * Otherwise, fall back to the scaleImage() method for resizing, which 510 * results in better image quality over resizeImage() with default filter 511 * settings and retains backward compatibility with pre 4.5 functionality. 512 */ 513 if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) { 514 $this->image->setOption( 'filter:support', '2.0' ); 515 $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 ); 516 } else { 517 $this->image->scaleImage( $dst_w, $dst_h ); 518 } 519 520 // Set appropriate quality settings after resizing. 521 if ( 'image/jpeg' === $this->mime_type ) { 522 if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) { 523 $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 ); 524 } 525 526 $this->image->setOption( 'jpeg:fancy-upsampling', 'off' ); 527 } 528 529 if ( $is_png ) { 530 $this->image->setOption( 'png:compression-filter', '5' ); 531 $this->image->setOption( 'png:compression-level', '9' ); 532 $this->image->setOption( 'png:compression-strategy', '1' ); 533 534 // Indexed PNG files get some additional handling. 535 // See #63448 for details. 536 if ( $is_indexed_png ) { 537 538 // Check for an alpha channel. 539 if ( $is_indexed_png_with_alpha_channel ) { 540 $this->image->setOption( 'png:include-chunk', 'tRNS' ); 541 } else { 542 $this->image->setOption( 'png:exclude-chunk', 'all' ); 543 } 544 545 $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false ); 546 547 /* 548 * If the colorspace is 'gray', use the png8 format to ensure it stays indexed. 549 * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs, 550 * even though grayscale PNGs usually have considerably larger file sizes. 551 * But we can force ImageMagick to save the image as an indexed PNG instead, 552 * by telling it to use png8 format. 553 * 554 * Note that we need to first call quantizeImage() before checking getImageColorspace(), 555 * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images 556 * (and we have not found any other way to identify grayscale images). 557 * 558 * We need to avoid forcing indexed format for images with true alpha transparency, 559 * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG. 560 */ 561 if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) { 562 // Set the image format to Indexed PNG. 563 $this->image->setOption( 'png:format', 'png8' ); 564 } 565 } else { 566 $this->image->setOption( 'png:exclude-chunk', 'all' ); 567 } 568 } 569 570 /* 571 * If alpha channel is not defined, set it opaque. 572 * 573 * Note that Imagick::getImageAlphaChannel() is only available if Imagick 574 * has been compiled against ImageMagick version 6.4.0 or newer. 575 */ 576 if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 577 && is_callable( array( $this->image, 'setImageAlphaChannel' ) ) 578 && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' ) 579 && defined( 'Imagick::ALPHACHANNEL_OPAQUE' ) 580 ) { 581 if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) { 582 $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE ); 583 } 584 } 585 586 // Limit the bit depth of resized images. 587 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { 588 /** 589 * Filters the maximum bit depth of resized images. 590 * 591 * This filter only applies when resizing using the Imagick editor since GD 592 * does not support getting or setting bit depth. 593 * 594 * Use this to adjust the maximum bit depth of resized images. 595 * 596 * @since 6.8.0 597 * 598 * @param int $max_depth The maximum bit depth. Default is the input depth. 599 * @param int $image_depth The bit depth of the original image. 600 */ 601 $max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() ); 602 $this->image->setImageDepth( $max_depth ); 603 } 604 } catch ( Exception $e ) { 605 return new WP_Error( 'image_resize_error', $e->getMessage() ); 606 } 607 } 608 609 /** 610 * Create multiple smaller images from a single source. 611 * 612 * Attempts to create all sub-sizes and returns the meta data at the end. This 613 * may result in the server running out of resources. When it fails there may be few 614 * "orphaned" images left over as the meta data is never returned and saved. 615 * 616 * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates 617 * the new images one at a time and allows for the meta data to be saved after 618 * each new image is created. 619 * 620 * @since 3.5.0 621 * 622 * @param array $sizes { 623 * An array of image size data arrays. 624 * 625 * Either a height or width must be provided. 626 * If one of the two is set to null, the resize will 627 * maintain aspect ratio according to the provided dimension. 628 * 629 * @type array ...$0 { 630 * Array of height, width values, and whether to crop. 631 * 632 * @type int $width Image width. Optional if `$height` is specified. 633 * @type int $height Image height. Optional if `$width` is specified. 634 * @type bool|array $crop Optional. Whether to crop the image. Default false. 635 * } 636 * } 637 * @return array An array of resized images' metadata by size. 638 */ 639 public function multi_resize( $sizes ) { 640 $metadata = array(); 641 642 foreach ( $sizes as $size => $size_data ) { 643 $meta = $this->make_subsize( $size_data ); 644 645 if ( ! is_wp_error( $meta ) ) { 646 $metadata[ $size ] = $meta; 647 } 648 } 649 650 return $metadata; 651 } 652 653 /** 654 * Create an image sub-size and return the image meta data value for it. 655 * 656 * @since 5.3.0 657 * 658 * @param array $size_data { 659 * Array of size data. 660 * 661 * @type int $width The maximum width in pixels. 662 * @type int $height The maximum height in pixels. 663 * @type bool|array $crop Whether to crop the image to exact dimensions. 664 * } 665 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, 666 * WP_Error object on error. 667 */ 668 public function make_subsize( $size_data ) { 669 if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { 670 return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) ); 671 } 672 673 $orig_size = $this->size; 674 $orig_image = $this->image->getImage(); 675 676 if ( ! isset( $size_data['width'] ) ) { 677 $size_data['width'] = null; 678 } 679 680 if ( ! isset( $size_data['height'] ) ) { 681 $size_data['height'] = null; 682 } 683 684 if ( ! isset( $size_data['crop'] ) ) { 685 $size_data['crop'] = false; 686 } 687 688 if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) { 689 return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) ); 690 } 691 692 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); 693 694 if ( is_wp_error( $resized ) ) { 695 $saved = $resized; 696 } else { 697 $saved = $this->_save( $this->image ); 698 699 $this->image->clear(); 700 $this->image->destroy(); 701 $this->image = null; 702 } 703 704 $this->size = $orig_size; 705 $this->image = $orig_image; 706 707 if ( ! is_wp_error( $saved ) ) { 708 unset( $saved['path'] ); 709 } 710 711 return $saved; 712 } 713 714 /** 715 * Crops Image. 716 * 717 * @since 3.5.0 718 * 719 * @param int $src_x The start x position to crop from. 720 * @param int $src_y The start y position to crop from. 721 * @param int $src_w The width to crop. 722 * @param int $src_h The height to crop. 723 * @param int $dst_w Optional. The destination width. 724 * @param int $dst_h Optional. The destination height. 725 * @param bool $src_abs Optional. If the source crop points are absolute. 726 * @return true|WP_Error 727 */ 728 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { 729 if ( $src_abs ) { 730 $src_w -= $src_x; 731 $src_h -= $src_y; 732 } 733 734 try { 735 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); 736 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); 737 738 if ( $dst_w || $dst_h ) { 739 /* 740 * If destination width/height isn't specified, 741 * use same as width/height from source. 742 */ 743 if ( ! $dst_w ) { 744 $dst_w = $src_w; 745 } 746 if ( ! $dst_h ) { 747 $dst_h = $src_h; 748 } 749 750 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 751 if ( is_wp_error( $thumb_result ) ) { 752 return $thumb_result; 753 } 754 755 return $this->update_size(); 756 } 757 } catch ( Exception $e ) { 758 return new WP_Error( 'image_crop_error', $e->getMessage() ); 759 } 760 761 return $this->update_size(); 762 } 763 764 /** 765 * Rotates current image counter-clockwise by $angle. 766 * 767 * @since 3.5.0 768 * 769 * @param float $angle 770 * @return true|WP_Error 771 */ 772 public function rotate( $angle ) { 773 /** 774 * $angle is 360-$angle because Imagick rotates clockwise 775 * (GD rotates counter-clockwise) 776 */ 777 try { 778 $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle ); 779 780 // Normalize EXIF orientation data so that display is consistent across devices. 781 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 782 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 783 } 784 785 // Since this changes the dimensions of the image, update the size. 786 $result = $this->update_size(); 787 if ( is_wp_error( $result ) ) { 788 return $result; 789 } 790 791 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); 792 } catch ( Exception $e ) { 793 return new WP_Error( 'image_rotate_error', $e->getMessage() ); 794 } 795 796 return true; 797 } 798 799 /** 800 * Flips current image. 801 * 802 * @since 3.5.0 803 * 804 * @param bool $horz Flip along Horizontal Axis 805 * @param bool $vert Flip along Vertical Axis 806 * @return true|WP_Error 807 */ 808 public function flip( $horz, $vert ) { 809 try { 810 if ( $horz ) { 811 $this->image->flipImage(); 812 } 813 814 if ( $vert ) { 815 $this->image->flopImage(); 816 } 817 818 // Normalize EXIF orientation data so that display is consistent across devices. 819 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 820 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 821 } 822 } catch ( Exception $e ) { 823 return new WP_Error( 'image_flip_error', $e->getMessage() ); 824 } 825 826 return true; 827 } 828 829 /** 830 * Check if a JPEG image has EXIF Orientation tag and rotate it if needed. 831 * 832 * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only 833 * if EXIF Orientation can be reset afterwards. 834 * 835 * @since 5.3.0 836 * 837 * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation. 838 * WP_Error if error while rotating. 839 */ 840 public function maybe_exif_rotate() { 841 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 842 return parent::maybe_exif_rotate(); 843 } else { 844 return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) ); 845 } 846 } 847 848 /** 849 * Saves current image to file. 850 * 851 * @since 3.5.0 852 * @since 6.0.0 The `$filesize` value was added to the returned array. 853 * 854 * @param string $destfilename Optional. Destination filename. Default null. 855 * @param string $mime_type Optional. The mime-type. Default null. 856 * @return array|WP_Error { 857 * Array on success or WP_Error if the file failed to save. 858 * 859 * @type string $path Path to the image file. 860 * @type string $file Name of the image file. 861 * @type int $width Image width. 862 * @type int $height Image height. 863 * @type string $mime-type The mime type of the image. 864 * @type int $filesize File size of the image. 865 * } 866 */ 867 public function save( $destfilename = null, $mime_type = null ) { 868 $saved = $this->_save( $this->image, $destfilename, $mime_type ); 869 870 if ( ! is_wp_error( $saved ) ) { 871 $this->file = $saved['path']; 872 $this->mime_type = $saved['mime-type']; 873 874 try { 875 $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) ); 876 } catch ( Exception $e ) { 877 return new WP_Error( 'image_save_error', $e->getMessage(), $this->file ); 878 } 879 } 880 881 return $saved; 882 } 883 884 /** 885 * Removes PDF alpha after it's been read. 886 * 887 * @since 6.4.0 888 */ 889 protected function remove_pdf_alpha_channel() { 890 $version = Imagick::getVersion(); 891 // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5. 892 if ( $version['versionNumber'] >= 0x675 ) { 893 try { 894 // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2. 895 $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 ); 896 } catch ( Exception $e ) { 897 return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() ); 898 } 899 } 900 } 901 902 /** 903 * @since 3.5.0 904 * @since 6.0.0 The `$filesize` value was added to the returned array. 905 * 906 * @param Imagick $image 907 * @param string $filename 908 * @param string $mime_type 909 * @return array|WP_Error { 910 * Array on success or WP_Error if the file failed to save. 911 * 912 * @type string $path Path to the image file. 913 * @type string $file Name of the image file. 914 * @type int $width Image width. 915 * @type int $height Image height. 916 * @type string $mime-type The mime type of the image. 917 * @type int $filesize File size of the image. 918 * } 919 */ 920 protected function _save( $image, $filename = null, $mime_type = null ) { 921 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); 922 923 if ( ! $filename ) { 924 $filename = $this->generate_filename( null, null, $extension ); 925 } 926 927 try { 928 // Store initial format. 929 $orig_format = $this->image->getImageFormat(); 930 931 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); 932 } catch ( Exception $e ) { 933 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 934 } 935 936 if ( method_exists( $this->image, 'setInterlaceScheme' ) 937 && method_exists( $this->image, 'getInterlaceScheme' ) 938 && defined( 'Imagick::INTERLACE_PLANE' ) 939 ) { 940 $orig_interlace = $this->image->getInterlaceScheme(); 941 942 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 943 if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) { 944 $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output. 945 } else { 946 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output. 947 } 948 } 949 950 $write_image_result = $this->write_image( $this->image, $filename ); 951 if ( is_wp_error( $write_image_result ) ) { 952 return $write_image_result; 953 } 954 955 try { 956 // Reset original format. 957 $this->image->setImageFormat( $orig_format ); 958 959 if ( isset( $orig_interlace ) ) { 960 $this->image->setInterlaceScheme( $orig_interlace ); 961 } 962 } catch ( Exception $e ) { 963 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 964 } 965 966 // Set correct file permissions. 967 $stat = stat( dirname( $filename ) ); 968 $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. 969 chmod( $filename, $perms ); 970 971 return array( 972 'path' => $filename, 973 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 974 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ), 975 'width' => $this->size['width'], 976 'height' => $this->size['height'], 977 'mime-type' => $mime_type, 978 'filesize' => wp_filesize( $filename ), 979 ); 980 } 981 982 /** 983 * Writes an image to a file or stream. 984 * 985 * @since 5.6.0 986 * 987 * @param Imagick $image 988 * @param string $filename The destination filename or stream URL. 989 * @return true|WP_Error 990 */ 991 private function write_image( $image, $filename ) { 992 if ( wp_is_stream( $filename ) ) { 993 /* 994 * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. 995 * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php 996 */ 997 if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { 998 return new WP_Error( 999 'image_save_error', 1000 sprintf( 1001 /* translators: %s: PHP function name. */ 1002 __( '%s failed while writing image to stream.' ), 1003 '<code>file_put_contents()</code>' 1004 ), 1005 $filename 1006 ); 1007 } else { 1008 return true; 1009 } 1010 } else { 1011 $dirname = dirname( $filename ); 1012 1013 if ( ! wp_mkdir_p( $dirname ) ) { 1014 return new WP_Error( 1015 'image_save_error', 1016 sprintf( 1017 /* translators: %s: Directory path. */ 1018 __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), 1019 esc_html( $dirname ) 1020 ) 1021 ); 1022 } 1023 1024 try { 1025 return $image->writeImage( $filename ); 1026 } catch ( Exception $e ) { 1027 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 1028 } 1029 } 1030 } 1031 1032 /** 1033 * Streams current image to browser. 1034 * 1035 * @since 3.5.0 1036 * 1037 * @param string $mime_type The mime type of the image. 1038 * @return true|WP_Error True on success, WP_Error object on failure. 1039 */ 1040 public function stream( $mime_type = null ) { 1041 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); 1042 1043 try { 1044 // Temporarily change format for stream. 1045 $this->image->setImageFormat( strtoupper( $extension ) ); 1046 1047 // Output stream of image content. 1048 header( "Content-Type: $mime_type" ); 1049 print $this->image->getImageBlob(); 1050 1051 // Reset image to original format. 1052 $this->image->setImageFormat( $this->get_extension( $this->mime_type ) ); 1053 } catch ( Exception $e ) { 1054 return new WP_Error( 'image_stream_error', $e->getMessage() ); 1055 } 1056 1057 return true; 1058 } 1059 1060 /** 1061 * Strips all image meta except color profiles from an image. 1062 * 1063 * @since 4.5.0 1064 * 1065 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. 1066 */ 1067 protected function strip_meta() { 1068 1069 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { 1070 return new WP_Error( 1071 'image_strip_meta_error', 1072 sprintf( 1073 /* translators: %s: ImageMagick method name. */ 1074 __( '%s is required to strip image meta.' ), 1075 '<code>Imagick::getImageProfiles()</code>' 1076 ) 1077 ); 1078 } 1079 1080 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { 1081 return new WP_Error( 1082 'image_strip_meta_error', 1083 sprintf( 1084 /* translators: %s: ImageMagick method name. */ 1085 __( '%s is required to strip image meta.' ), 1086 '<code>Imagick::removeImageProfile()</code>' 1087 ) 1088 ); 1089 } 1090 1091 /* 1092 * Protect a few profiles from being stripped for the following reasons: 1093 * 1094 * - icc: Color profile information 1095 * - icm: Color profile information 1096 * - iptc: Copyright data 1097 * - exif: Orientation data 1098 * - xmp: Rights usage data 1099 */ 1100 $protected_profiles = array( 1101 'icc', 1102 'icm', 1103 'iptc', 1104 'exif', 1105 'xmp', 1106 ); 1107 1108 try { 1109 // Strip profiles. 1110 foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) { 1111 if ( ! in_array( $key, $protected_profiles, true ) ) { 1112 $this->image->removeImageProfile( $key ); 1113 } 1114 } 1115 } catch ( Exception $e ) { 1116 return new WP_Error( 'image_strip_meta_error', $e->getMessage() ); 1117 } 1118 1119 return true; 1120 } 1121 1122 /** 1123 * Sets up Imagick for PDF processing. 1124 * Increases rendering DPI and only loads first page. 1125 * 1126 * @since 4.7.0 1127 * 1128 * @return string|WP_Error File to load or WP_Error on failure. 1129 */ 1130 protected function pdf_setup() { 1131 try { 1132 /* 1133 * By default, PDFs are rendered in a very low resolution. 1134 * We want the thumbnail to be readable, so increase the rendering DPI. 1135 */ 1136 $this->image->setResolution( 128, 128 ); 1137 1138 // Only load the first page. 1139 return $this->file . '[0]'; 1140 } catch ( Exception $e ) { 1141 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); 1142 } 1143 } 1144 1145 /** 1146 * Load the image produced by Ghostscript. 1147 * 1148 * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files 1149 * when `use-cropbox` is set. 1150 * 1151 * @since 5.6.0 1152 * 1153 * @return true|WP_Error 1154 */ 1155 protected function pdf_load_source() { 1156 $filename = $this->pdf_setup(); 1157 1158 if ( is_wp_error( $filename ) ) { 1159 return $filename; 1160 } 1161 1162 try { 1163 /* 1164 * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped 1165 * area (resulting in unnecessary whitespace) unless the following option is set. 1166 */ 1167 $this->image->setOption( 'pdf:use-cropbox', true ); 1168 1169 /* 1170 * Reading image after Imagick instantiation because `setResolution` 1171 * only applies correctly before the image is read. 1172 */ 1173 $this->image->readImage( $filename ); 1174 } catch ( Exception $e ) { 1175 // Attempt to run `gs` without the `use-cropbox` option. See #48853. 1176 $this->image->setOption( 'pdf:use-cropbox', false ); 1177 1178 $this->image->readImage( $filename ); 1179 } 1180 1181 return true; 1182 } 1183 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Fri Jun 19 08:20:10 2026 | Cross-referenced by PHPXref |