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