[ 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 * 194 * @param int $quality Compression Quality. Range: [1,100] 195 * @return true|WP_Error True if set successfully; WP_Error on failure. 196 */ 197 public function set_quality( $quality = null ) { 198 $quality_result = parent::set_quality( $quality ); 199 if ( is_wp_error( $quality_result ) ) { 200 return $quality_result; 201 } else { 202 $quality = $this->get_quality(); 203 } 204 205 try { 206 switch ( $this->mime_type ) { 207 case 'image/jpeg': 208 $this->image->setImageCompressionQuality( $quality ); 209 $this->image->setCompressionQuality( $quality ); 210 $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); 211 break; 212 case 'image/webp': 213 $webp_info = wp_get_webp_info( $this->file ); 214 215 if ( 'lossless' === $webp_info['type'] ) { 216 // Use WebP lossless settings. 217 $this->image->setImageCompressionQuality( 100 ); 218 $this->image->setCompressionQuality( 100 ); 219 $this->image->setOption( 'webp:lossless', 'true' ); 220 parent::set_quality( 100 ); 221 } else { 222 $this->image->setImageCompressionQuality( $quality ); 223 $this->image->setCompressionQuality( $quality ); 224 } 225 break; 226 case 'image/avif': 227 // Set the AVIF encoder to work faster, with minimal impact on image size. 228 $this->image->setOption( 'heic:speed', 7 ); 229 $this->image->setImageCompressionQuality( $quality ); 230 $this->image->setCompressionQuality( $quality ); 231 break; 232 default: 233 $this->image->setImageCompressionQuality( $quality ); 234 $this->image->setCompressionQuality( $quality ); 235 } 236 } catch ( Exception $e ) { 237 return new WP_Error( 'image_quality_error', $e->getMessage() ); 238 } 239 return true; 240 } 241 242 243 /** 244 * Sets or updates current image size. 245 * 246 * @since 3.5.0 247 * 248 * @param int $width 249 * @param int $height 250 * @return true|WP_Error 251 */ 252 protected function update_size( $width = null, $height = null ) { 253 $size = null; 254 if ( ! $width || ! $height ) { 255 try { 256 $size = $this->image->getImageGeometry(); 257 } catch ( Exception $e ) { 258 return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file ); 259 } 260 } 261 262 if ( ! $width ) { 263 $width = $size['width']; 264 } 265 266 if ( ! $height ) { 267 $height = $size['height']; 268 } 269 270 /* 271 * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images 272 * are properly sized without affecting previous `getImageGeometry` behavior. 273 */ 274 if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) { 275 $size = wp_getimagesize( $this->file ); 276 $width = $size[0]; 277 $height = $size[1]; 278 } 279 280 return parent::update_size( $width, $height ); 281 } 282 283 /** 284 * Sets Imagick time limit. 285 * 286 * Depending on configuration, Imagick processing may take time. 287 * 288 * Multiple problems exist if PHP times out before ImageMagick completed: 289 * 1. Temporary files aren't cleaned by ImageMagick garbage collection. 290 * 2. No clear error is provided. 291 * 3. The cause of such timeout can be hard to pinpoint. 292 * 293 * This function, which is expected to be run before heavy image routines, resolves 294 * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set. 295 * 296 * However seems it introduces more problems than it fixes, 297 * see https://core.trac.wordpress.org/ticket/58202. 298 * 299 * Note: 300 * - Imagick resource exhaustion does not issue catchable exceptions (yet). 301 * See https://github.com/Imagick/imagick/issues/333. 302 * - The resource limit is not saved/restored. It applies to subsequent 303 * image operations within the time of the HTTP request. 304 * 305 * @since 6.2.0 306 * @since 6.3.0 This method was deprecated. 307 * 308 * @return int|null The new limit on success, null on failure. 309 */ 310 public static function set_imagick_time_limit() { 311 _deprecated_function( __METHOD__, '6.3.0' ); 312 313 if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) { 314 return null; 315 } 316 317 // Returns PHP_FLOAT_MAX if unset. 318 $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME ); 319 320 // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX. 321 $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout; 322 323 $php_timeout = (int) ini_get( 'max_execution_time' ); 324 325 if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) { 326 $limit = (float) 0.8 * $php_timeout; 327 Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit ); 328 329 return $limit; 330 } 331 } 332 333 /** 334 * Resizes current image. 335 * 336 * At minimum, either a height or width must be provided. 337 * If one of the two is set to null, the resize will 338 * maintain aspect ratio according to the provided dimension. 339 * 340 * @since 3.5.0 341 * 342 * @param int|null $max_w Image width. 343 * @param int|null $max_h Image height. 344 * @param bool|array $crop { 345 * Optional. Image cropping behavior. If false, the image will be scaled (default). 346 * If true, image will be cropped to the specified dimensions using center positions. 347 * If an array, the image will be cropped using the array to specify the crop location: 348 * 349 * @type string $0 The x crop position. Accepts 'left', 'center', or 'right'. 350 * @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'. 351 * } 352 * @return true|WP_Error 353 */ 354 public function resize( $max_w, $max_h, $crop = false ) { 355 if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) { 356 return true; 357 } 358 359 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); 360 if ( ! $dims ) { 361 return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) ); 362 } 363 364 list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; 365 366 if ( $crop ) { 367 return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); 368 } 369 370 // Execute the resize. 371 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 372 if ( is_wp_error( $thumb_result ) ) { 373 return $thumb_result; 374 } 375 376 return $this->update_size( $dst_w, $dst_h ); 377 } 378 379 /** 380 * Efficiently resize the current image 381 * 382 * This is a WordPress specific implementation of Imagick::thumbnailImage(), 383 * which resizes an image to given dimensions and removes any associated profiles. 384 * 385 * @since 4.5.0 386 * 387 * @param int $dst_w The destination width. 388 * @param int $dst_h The destination height. 389 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. 390 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. 391 * @return void|WP_Error 392 */ 393 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { 394 $allowed_filters = array( 395 'FILTER_POINT', 396 'FILTER_BOX', 397 'FILTER_TRIANGLE', 398 'FILTER_HERMITE', 399 'FILTER_HANNING', 400 'FILTER_HAMMING', 401 'FILTER_BLACKMAN', 402 'FILTER_GAUSSIAN', 403 'FILTER_QUADRATIC', 404 'FILTER_CUBIC', 405 'FILTER_CATROM', 406 'FILTER_MITCHELL', 407 'FILTER_LANCZOS', 408 'FILTER_BESSEL', 409 'FILTER_SINC', 410 ); 411 412 /** 413 * Set the filter value if '$filter_name' name is in the allowed list and the related 414 * Imagick constant is defined or fall back to the default filter. 415 */ 416 if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) { 417 $filter = constant( 'Imagick::' . $filter_name ); 418 } else { 419 $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false; 420 } 421 422 /** 423 * Filters whether to strip metadata from images when they're resized. 424 * 425 * This filter only applies when resizing using the Imagick editor since GD 426 * always strips profiles by default. 427 * 428 * @since 4.5.0 429 * 430 * @param bool $strip_meta Whether to strip image metadata during resizing. Default true. 431 */ 432 if ( apply_filters( 'image_strip_meta', $strip_meta ) ) { 433 $this->strip_meta(); // Fail silently if not supported. 434 } 435 436 try { 437 /* 438 * To be more efficient, resample large images to 5x the destination size before resizing 439 * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111), 440 * unless we would be resampling to a scale smaller than 128x128. 441 */ 442 if ( is_callable( array( $this->image, 'sampleImage' ) ) ) { 443 $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] ); 444 $sample_factor = 5; 445 446 if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) { 447 $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor ); 448 } 449 } 450 451 /* 452 * Use resizeImage() when it's available and a valid filter value is set. 453 * Otherwise, fall back to the scaleImage() method for resizing, which 454 * results in better image quality over resizeImage() with default filter 455 * settings and retains backward compatibility with pre 4.5 functionality. 456 */ 457 if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) { 458 $this->image->setOption( 'filter:support', '2.0' ); 459 $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 ); 460 } else { 461 $this->image->scaleImage( $dst_w, $dst_h ); 462 } 463 464 // Set appropriate quality settings after resizing. 465 if ( 'image/jpeg' === $this->mime_type ) { 466 if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) { 467 $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 ); 468 } 469 470 $this->image->setOption( 'jpeg:fancy-upsampling', 'off' ); 471 } 472 473 if ( 'image/png' === $this->mime_type ) { 474 $this->image->setOption( 'png:compression-filter', '5' ); 475 $this->image->setOption( 'png:compression-level', '9' ); 476 $this->image->setOption( 'png:compression-strategy', '1' ); 477 $this->image->setOption( 'png:exclude-chunk', 'all' ); 478 } 479 480 /* 481 * If alpha channel is not defined, set it opaque. 482 * 483 * Note that Imagick::getImageAlphaChannel() is only available if Imagick 484 * has been compiled against ImageMagick version 6.4.0 or newer. 485 */ 486 if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 487 && is_callable( array( $this->image, 'setImageAlphaChannel' ) ) 488 && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' ) 489 && defined( 'Imagick::ALPHACHANNEL_OPAQUE' ) 490 ) { 491 if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) { 492 $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE ); 493 } 494 } 495 496 // Limit the bit depth of resized images to 8 bits per channel. 497 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { 498 if ( 8 < $this->image->getImageDepth() ) { 499 $this->image->setImageDepth( 8 ); 500 } 501 } 502 } catch ( Exception $e ) { 503 return new WP_Error( 'image_resize_error', $e->getMessage() ); 504 } 505 } 506 507 /** 508 * Create multiple smaller images from a single source. 509 * 510 * Attempts to create all sub-sizes and returns the meta data at the end. This 511 * may result in the server running out of resources. When it fails there may be few 512 * "orphaned" images left over as the meta data is never returned and saved. 513 * 514 * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates 515 * the new images one at a time and allows for the meta data to be saved after 516 * each new image is created. 517 * 518 * @since 3.5.0 519 * 520 * @param array $sizes { 521 * An array of image size data arrays. 522 * 523 * Either a height or width must be provided. 524 * If one of the two is set to null, the resize will 525 * maintain aspect ratio according to the provided dimension. 526 * 527 * @type array ...$0 { 528 * Array of height, width values, and whether to crop. 529 * 530 * @type int $width Image width. Optional if `$height` is specified. 531 * @type int $height Image height. Optional if `$width` is specified. 532 * @type bool|array $crop Optional. Whether to crop the image. Default false. 533 * } 534 * } 535 * @return array An array of resized images' metadata by size. 536 */ 537 public function multi_resize( $sizes ) { 538 $metadata = array(); 539 540 foreach ( $sizes as $size => $size_data ) { 541 $meta = $this->make_subsize( $size_data ); 542 543 if ( ! is_wp_error( $meta ) ) { 544 $metadata[ $size ] = $meta; 545 } 546 } 547 548 return $metadata; 549 } 550 551 /** 552 * Create an image sub-size and return the image meta data value for it. 553 * 554 * @since 5.3.0 555 * 556 * @param array $size_data { 557 * Array of size data. 558 * 559 * @type int $width The maximum width in pixels. 560 * @type int $height The maximum height in pixels. 561 * @type bool|array $crop Whether to crop the image to exact dimensions. 562 * } 563 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, 564 * WP_Error object on error. 565 */ 566 public function make_subsize( $size_data ) { 567 if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { 568 return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) ); 569 } 570 571 $orig_size = $this->size; 572 $orig_image = $this->image->getImage(); 573 574 if ( ! isset( $size_data['width'] ) ) { 575 $size_data['width'] = null; 576 } 577 578 if ( ! isset( $size_data['height'] ) ) { 579 $size_data['height'] = null; 580 } 581 582 if ( ! isset( $size_data['crop'] ) ) { 583 $size_data['crop'] = false; 584 } 585 586 if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) { 587 return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) ); 588 } 589 590 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); 591 592 if ( is_wp_error( $resized ) ) { 593 $saved = $resized; 594 } else { 595 $saved = $this->_save( $this->image ); 596 597 $this->image->clear(); 598 $this->image->destroy(); 599 $this->image = null; 600 } 601 602 $this->size = $orig_size; 603 $this->image = $orig_image; 604 605 if ( ! is_wp_error( $saved ) ) { 606 unset( $saved['path'] ); 607 } 608 609 return $saved; 610 } 611 612 /** 613 * Crops Image. 614 * 615 * @since 3.5.0 616 * 617 * @param int $src_x The start x position to crop from. 618 * @param int $src_y The start y position to crop from. 619 * @param int $src_w The width to crop. 620 * @param int $src_h The height to crop. 621 * @param int $dst_w Optional. The destination width. 622 * @param int $dst_h Optional. The destination height. 623 * @param bool $src_abs Optional. If the source crop points are absolute. 624 * @return true|WP_Error 625 */ 626 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { 627 if ( $src_abs ) { 628 $src_w -= $src_x; 629 $src_h -= $src_y; 630 } 631 632 try { 633 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); 634 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); 635 636 if ( $dst_w || $dst_h ) { 637 /* 638 * If destination width/height isn't specified, 639 * use same as width/height from source. 640 */ 641 if ( ! $dst_w ) { 642 $dst_w = $src_w; 643 } 644 if ( ! $dst_h ) { 645 $dst_h = $src_h; 646 } 647 648 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 649 if ( is_wp_error( $thumb_result ) ) { 650 return $thumb_result; 651 } 652 653 return $this->update_size(); 654 } 655 } catch ( Exception $e ) { 656 return new WP_Error( 'image_crop_error', $e->getMessage() ); 657 } 658 659 return $this->update_size(); 660 } 661 662 /** 663 * Rotates current image counter-clockwise by $angle. 664 * 665 * @since 3.5.0 666 * 667 * @param float $angle 668 * @return true|WP_Error 669 */ 670 public function rotate( $angle ) { 671 /** 672 * $angle is 360-$angle because Imagick rotates clockwise 673 * (GD rotates counter-clockwise) 674 */ 675 try { 676 $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle ); 677 678 // Normalize EXIF orientation data so that display is consistent across devices. 679 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 680 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 681 } 682 683 // Since this changes the dimensions of the image, update the size. 684 $result = $this->update_size(); 685 if ( is_wp_error( $result ) ) { 686 return $result; 687 } 688 689 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); 690 } catch ( Exception $e ) { 691 return new WP_Error( 'image_rotate_error', $e->getMessage() ); 692 } 693 694 return true; 695 } 696 697 /** 698 * Flips current image. 699 * 700 * @since 3.5.0 701 * 702 * @param bool $horz Flip along Horizontal Axis 703 * @param bool $vert Flip along Vertical Axis 704 * @return true|WP_Error 705 */ 706 public function flip( $horz, $vert ) { 707 try { 708 if ( $horz ) { 709 $this->image->flipImage(); 710 } 711 712 if ( $vert ) { 713 $this->image->flopImage(); 714 } 715 716 // Normalize EXIF orientation data so that display is consistent across devices. 717 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 718 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 719 } 720 } catch ( Exception $e ) { 721 return new WP_Error( 'image_flip_error', $e->getMessage() ); 722 } 723 724 return true; 725 } 726 727 /** 728 * Check if a JPEG image has EXIF Orientation tag and rotate it if needed. 729 * 730 * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only 731 * if EXIF Orientation can be reset afterwards. 732 * 733 * @since 5.3.0 734 * 735 * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation. 736 * WP_Error if error while rotating. 737 */ 738 public function maybe_exif_rotate() { 739 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 740 return parent::maybe_exif_rotate(); 741 } else { 742 return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) ); 743 } 744 } 745 746 /** 747 * Saves current image to file. 748 * 749 * @since 3.5.0 750 * @since 6.0.0 The `$filesize` value was added to the returned array. 751 * 752 * @param string $destfilename Optional. Destination filename. Default null. 753 * @param string $mime_type Optional. The mime-type. Default null. 754 * @return array|WP_Error { 755 * Array on success or WP_Error if the file failed to save. 756 * 757 * @type string $path Path to the image file. 758 * @type string $file Name of the image file. 759 * @type int $width Image width. 760 * @type int $height Image height. 761 * @type string $mime-type The mime type of the image. 762 * @type int $filesize File size of the image. 763 * } 764 */ 765 public function save( $destfilename = null, $mime_type = null ) { 766 $saved = $this->_save( $this->image, $destfilename, $mime_type ); 767 768 if ( ! is_wp_error( $saved ) ) { 769 $this->file = $saved['path']; 770 $this->mime_type = $saved['mime-type']; 771 772 try { 773 $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) ); 774 } catch ( Exception $e ) { 775 return new WP_Error( 'image_save_error', $e->getMessage(), $this->file ); 776 } 777 } 778 779 return $saved; 780 } 781 782 /** 783 * Removes PDF alpha after it's been read. 784 * 785 * @since 6.4.0 786 */ 787 protected function remove_pdf_alpha_channel() { 788 $version = Imagick::getVersion(); 789 // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5. 790 if ( $version['versionNumber'] >= 0x675 ) { 791 try { 792 // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2. 793 $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 ); 794 } catch ( Exception $e ) { 795 return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() ); 796 } 797 } 798 } 799 800 /** 801 * @since 3.5.0 802 * @since 6.0.0 The `$filesize` value was added to the returned array. 803 * 804 * @param Imagick $image 805 * @param string $filename 806 * @param string $mime_type 807 * @return array|WP_Error { 808 * Array on success or WP_Error if the file failed to save. 809 * 810 * @type string $path Path to the image file. 811 * @type string $file Name of the image file. 812 * @type int $width Image width. 813 * @type int $height Image height. 814 * @type string $mime-type The mime type of the image. 815 * @type int $filesize File size of the image. 816 * } 817 */ 818 protected function _save( $image, $filename = null, $mime_type = null ) { 819 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); 820 821 if ( ! $filename ) { 822 $filename = $this->generate_filename( null, null, $extension ); 823 } 824 825 try { 826 // Store initial format. 827 $orig_format = $this->image->getImageFormat(); 828 829 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); 830 } catch ( Exception $e ) { 831 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 832 } 833 834 if ( method_exists( $this->image, 'setInterlaceScheme' ) 835 && method_exists( $this->image, 'getInterlaceScheme' ) 836 && defined( 'Imagick::INTERLACE_PLANE' ) 837 ) { 838 $orig_interlace = $this->image->getInterlaceScheme(); 839 840 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 841 if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) { 842 $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output. 843 } else { 844 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output. 845 } 846 } 847 848 $write_image_result = $this->write_image( $this->image, $filename ); 849 if ( is_wp_error( $write_image_result ) ) { 850 return $write_image_result; 851 } 852 853 try { 854 // Reset original format. 855 $this->image->setImageFormat( $orig_format ); 856 857 if ( isset( $orig_interlace ) ) { 858 $this->image->setInterlaceScheme( $orig_interlace ); 859 } 860 } catch ( Exception $e ) { 861 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 862 } 863 864 // Set correct file permissions. 865 $stat = stat( dirname( $filename ) ); 866 $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. 867 chmod( $filename, $perms ); 868 869 return array( 870 'path' => $filename, 871 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 872 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ), 873 'width' => $this->size['width'], 874 'height' => $this->size['height'], 875 'mime-type' => $mime_type, 876 'filesize' => wp_filesize( $filename ), 877 ); 878 } 879 880 /** 881 * Writes an image to a file or stream. 882 * 883 * @since 5.6.0 884 * 885 * @param Imagick $image 886 * @param string $filename The destination filename or stream URL. 887 * @return true|WP_Error 888 */ 889 private function write_image( $image, $filename ) { 890 if ( wp_is_stream( $filename ) ) { 891 /* 892 * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. 893 * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php 894 */ 895 if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { 896 return new WP_Error( 897 'image_save_error', 898 sprintf( 899 /* translators: %s: PHP function name. */ 900 __( '%s failed while writing image to stream.' ), 901 '<code>file_put_contents()</code>' 902 ), 903 $filename 904 ); 905 } else { 906 return true; 907 } 908 } else { 909 $dirname = dirname( $filename ); 910 911 if ( ! wp_mkdir_p( $dirname ) ) { 912 return new WP_Error( 913 'image_save_error', 914 sprintf( 915 /* translators: %s: Directory path. */ 916 __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), 917 esc_html( $dirname ) 918 ) 919 ); 920 } 921 922 try { 923 return $image->writeImage( $filename ); 924 } catch ( Exception $e ) { 925 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 926 } 927 } 928 } 929 930 /** 931 * Streams current image to browser. 932 * 933 * @since 3.5.0 934 * 935 * @param string $mime_type The mime type of the image. 936 * @return true|WP_Error True on success, WP_Error object on failure. 937 */ 938 public function stream( $mime_type = null ) { 939 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); 940 941 try { 942 // Temporarily change format for stream. 943 $this->image->setImageFormat( strtoupper( $extension ) ); 944 945 // Output stream of image content. 946 header( "Content-Type: $mime_type" ); 947 print $this->image->getImageBlob(); 948 949 // Reset image to original format. 950 $this->image->setImageFormat( $this->get_extension( $this->mime_type ) ); 951 } catch ( Exception $e ) { 952 return new WP_Error( 'image_stream_error', $e->getMessage() ); 953 } 954 955 return true; 956 } 957 958 /** 959 * Strips all image meta except color profiles from an image. 960 * 961 * @since 4.5.0 962 * 963 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. 964 */ 965 protected function strip_meta() { 966 967 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { 968 return new WP_Error( 969 'image_strip_meta_error', 970 sprintf( 971 /* translators: %s: ImageMagick method name. */ 972 __( '%s is required to strip image meta.' ), 973 '<code>Imagick::getImageProfiles()</code>' 974 ) 975 ); 976 } 977 978 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { 979 return new WP_Error( 980 'image_strip_meta_error', 981 sprintf( 982 /* translators: %s: ImageMagick method name. */ 983 __( '%s is required to strip image meta.' ), 984 '<code>Imagick::removeImageProfile()</code>' 985 ) 986 ); 987 } 988 989 /* 990 * Protect a few profiles from being stripped for the following reasons: 991 * 992 * - icc: Color profile information 993 * - icm: Color profile information 994 * - iptc: Copyright data 995 * - exif: Orientation data 996 * - xmp: Rights usage data 997 */ 998 $protected_profiles = array( 999 'icc', 1000 'icm', 1001 'iptc', 1002 'exif', 1003 'xmp', 1004 ); 1005 1006 try { 1007 // Strip profiles. 1008 foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) { 1009 if ( ! in_array( $key, $protected_profiles, true ) ) { 1010 $this->image->removeImageProfile( $key ); 1011 } 1012 } 1013 } catch ( Exception $e ) { 1014 return new WP_Error( 'image_strip_meta_error', $e->getMessage() ); 1015 } 1016 1017 return true; 1018 } 1019 1020 /** 1021 * Sets up Imagick for PDF processing. 1022 * Increases rendering DPI and only loads first page. 1023 * 1024 * @since 4.7.0 1025 * 1026 * @return string|WP_Error File to load or WP_Error on failure. 1027 */ 1028 protected function pdf_setup() { 1029 try { 1030 /* 1031 * By default, PDFs are rendered in a very low resolution. 1032 * We want the thumbnail to be readable, so increase the rendering DPI. 1033 */ 1034 $this->image->setResolution( 128, 128 ); 1035 1036 // Only load the first page. 1037 return $this->file . '[0]'; 1038 } catch ( Exception $e ) { 1039 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); 1040 } 1041 } 1042 1043 /** 1044 * Load the image produced by Ghostscript. 1045 * 1046 * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files 1047 * when `use-cropbox` is set. 1048 * 1049 * @since 5.6.0 1050 * 1051 * @return true|WP_Error 1052 */ 1053 protected function pdf_load_source() { 1054 $filename = $this->pdf_setup(); 1055 1056 if ( is_wp_error( $filename ) ) { 1057 return $filename; 1058 } 1059 1060 try { 1061 /* 1062 * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped 1063 * area (resulting in unnecessary whitespace) unless the following option is set. 1064 */ 1065 $this->image->setOption( 'pdf:use-cropbox', true ); 1066 1067 /* 1068 * Reading image after Imagick instantiation because `setResolution` 1069 * only applies correctly before the image is read. 1070 */ 1071 $this->image->readImage( $filename ); 1072 } catch ( Exception $e ) { 1073 // Attempt to run `gs` without the `use-cropbox` option. See #48853. 1074 $this->image->setOption( 'pdf:use-cropbox', false ); 1075 1076 $this->image->readImage( $filename ); 1077 } 1078 1079 return true; 1080 } 1081 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Thu Nov 21 08:20:01 2024 | Cross-referenced by PHPXref |