[ Index ] |
PHP Cross Reference of WordPress |
[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 // setIteratorIndex is optional unless mime is an animated format. 106 // Here, we just say no if you are missing it and aren't loading a jpeg. 107 if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) { 108 return false; 109 } 110 111 try { 112 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 113 return ( (bool) @Imagick::queryFormats( $imagick_extension ) ); 114 } catch ( Exception $e ) { 115 return false; 116 } 117 } 118 119 /** 120 * Loads image from $this->file into new Imagick Object. 121 * 122 * @since 3.5.0 123 * 124 * @return true|WP_Error True if loaded; WP_Error on failure. 125 */ 126 public function load() { 127 if ( $this->image instanceof Imagick ) { 128 return true; 129 } 130 131 if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) { 132 return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file ); 133 } 134 135 /* 136 * Even though Imagick uses less PHP memory than GD, set higher limit 137 * for users that have low PHP.ini limits. 138 */ 139 wp_raise_memory_limit( 'image' ); 140 141 try { 142 $this->image = new Imagick(); 143 $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); 144 145 if ( 'pdf' === $file_extension ) { 146 $pdf_loaded = $this->pdf_load_source(); 147 148 if ( is_wp_error( $pdf_loaded ) ) { 149 return $pdf_loaded; 150 } 151 } else { 152 if ( wp_is_stream( $this->file ) ) { 153 // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead. 154 $this->image->readImageBlob( file_get_contents( $this->file ), $this->file ); 155 } else { 156 $this->image->readImage( $this->file ); 157 } 158 } 159 160 if ( ! $this->image->valid() ) { 161 return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); 162 } 163 164 // Select the first frame to handle animated images properly. 165 if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) { 166 $this->image->setIteratorIndex( 0 ); 167 } 168 169 $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() ); 170 } catch ( Exception $e ) { 171 return new WP_Error( 'invalid_image', $e->getMessage(), $this->file ); 172 } 173 174 $updated_size = $this->update_size(); 175 176 if ( is_wp_error( $updated_size ) ) { 177 return $updated_size; 178 } 179 180 return $this->set_quality(); 181 } 182 183 /** 184 * Sets Image Compression quality on a 1-100% scale. 185 * 186 * @since 3.5.0 187 * 188 * @param int $quality Compression Quality. Range: [1,100] 189 * @return true|WP_Error True if set successfully; WP_Error on failure. 190 */ 191 public function set_quality( $quality = null ) { 192 $quality_result = parent::set_quality( $quality ); 193 if ( is_wp_error( $quality_result ) ) { 194 return $quality_result; 195 } else { 196 $quality = $this->get_quality(); 197 } 198 199 try { 200 switch ( $this->mime_type ) { 201 case 'image/jpeg': 202 $this->image->setImageCompressionQuality( $quality ); 203 $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); 204 break; 205 case 'image/webp': 206 $webp_info = wp_get_webp_info( $this->file ); 207 208 if ( 'lossless' === $webp_info['type'] ) { 209 // Use WebP lossless settings. 210 $this->image->setImageCompressionQuality( 100 ); 211 $this->image->setOption( 'webp:lossless', 'true' ); 212 } else { 213 $this->image->setImageCompressionQuality( $quality ); 214 } 215 break; 216 default: 217 $this->image->setImageCompressionQuality( $quality ); 218 } 219 } catch ( Exception $e ) { 220 return new WP_Error( 'image_quality_error', $e->getMessage() ); 221 } 222 return true; 223 } 224 225 226 /** 227 * Sets or updates current image size. 228 * 229 * @since 3.5.0 230 * 231 * @param int $width 232 * @param int $height 233 * @return true|WP_Error 234 */ 235 protected function update_size( $width = null, $height = null ) { 236 $size = null; 237 if ( ! $width || ! $height ) { 238 try { 239 $size = $this->image->getImageGeometry(); 240 } catch ( Exception $e ) { 241 return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file ); 242 } 243 } 244 245 if ( ! $width ) { 246 $width = $size['width']; 247 } 248 249 if ( ! $height ) { 250 $height = $size['height']; 251 } 252 253 return parent::update_size( $width, $height ); 254 } 255 256 /** 257 * Resizes current image. 258 * 259 * At minimum, either a height or width must be provided. 260 * If one of the two is set to null, the resize will 261 * maintain aspect ratio according to the provided dimension. 262 * 263 * @since 3.5.0 264 * 265 * @param int|null $max_w Image width. 266 * @param int|null $max_h Image height. 267 * @param bool $crop 268 * @return true|WP_Error 269 */ 270 public function resize( $max_w, $max_h, $crop = false ) { 271 if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { 272 return true; 273 } 274 275 $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop ); 276 if ( ! $dims ) { 277 return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) ); 278 } 279 280 list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; 281 282 if ( $crop ) { 283 return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); 284 } 285 286 // Execute the resize. 287 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 288 if ( is_wp_error( $thumb_result ) ) { 289 return $thumb_result; 290 } 291 292 return $this->update_size( $dst_w, $dst_h ); 293 } 294 295 /** 296 * Efficiently resize the current image 297 * 298 * This is a WordPress specific implementation of Imagick::thumbnailImage(), 299 * which resizes an image to given dimensions and removes any associated profiles. 300 * 301 * @since 4.5.0 302 * 303 * @param int $dst_w The destination width. 304 * @param int $dst_h The destination height. 305 * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. 306 * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. 307 * @return void|WP_Error 308 */ 309 protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { 310 $allowed_filters = array( 311 'FILTER_POINT', 312 'FILTER_BOX', 313 'FILTER_TRIANGLE', 314 'FILTER_HERMITE', 315 'FILTER_HANNING', 316 'FILTER_HAMMING', 317 'FILTER_BLACKMAN', 318 'FILTER_GAUSSIAN', 319 'FILTER_QUADRATIC', 320 'FILTER_CUBIC', 321 'FILTER_CATROM', 322 'FILTER_MITCHELL', 323 'FILTER_LANCZOS', 324 'FILTER_BESSEL', 325 'FILTER_SINC', 326 ); 327 328 /** 329 * Set the filter value if '$filter_name' name is in the allowed list and the related 330 * Imagick constant is defined or fall back to the default filter. 331 */ 332 if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) { 333 $filter = constant( 'Imagick::' . $filter_name ); 334 } else { 335 $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false; 336 } 337 338 /** 339 * Filters whether to strip metadata from images when they're resized. 340 * 341 * This filter only applies when resizing using the Imagick editor since GD 342 * always strips profiles by default. 343 * 344 * @since 4.5.0 345 * 346 * @param bool $strip_meta Whether to strip image metadata during resizing. Default true. 347 */ 348 if ( apply_filters( 'image_strip_meta', $strip_meta ) ) { 349 $this->strip_meta(); // Fail silently if not supported. 350 } 351 352 try { 353 /* 354 * To be more efficient, resample large images to 5x the destination size before resizing 355 * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111), 356 * unless we would be resampling to a scale smaller than 128x128. 357 */ 358 if ( is_callable( array( $this->image, 'sampleImage' ) ) ) { 359 $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] ); 360 $sample_factor = 5; 361 362 if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) { 363 $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor ); 364 } 365 } 366 367 /* 368 * Use resizeImage() when it's available and a valid filter value is set. 369 * Otherwise, fall back to the scaleImage() method for resizing, which 370 * results in better image quality over resizeImage() with default filter 371 * settings and retains backward compatibility with pre 4.5 functionality. 372 */ 373 if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) { 374 $this->image->setOption( 'filter:support', '2.0' ); 375 $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 ); 376 } else { 377 $this->image->scaleImage( $dst_w, $dst_h ); 378 } 379 380 // Set appropriate quality settings after resizing. 381 if ( 'image/jpeg' === $this->mime_type ) { 382 if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) { 383 $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 ); 384 } 385 386 $this->image->setOption( 'jpeg:fancy-upsampling', 'off' ); 387 } 388 389 if ( 'image/png' === $this->mime_type ) { 390 $this->image->setOption( 'png:compression-filter', '5' ); 391 $this->image->setOption( 'png:compression-level', '9' ); 392 $this->image->setOption( 'png:compression-strategy', '1' ); 393 $this->image->setOption( 'png:exclude-chunk', 'all' ); 394 } 395 396 /* 397 * If alpha channel is not defined, set it opaque. 398 * 399 * Note that Imagick::getImageAlphaChannel() is only available if Imagick 400 * has been compiled against ImageMagick version 6.4.0 or newer. 401 */ 402 if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) ) 403 && is_callable( array( $this->image, 'setImageAlphaChannel' ) ) 404 && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' ) 405 && defined( 'Imagick::ALPHACHANNEL_OPAQUE' ) 406 ) { 407 if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) { 408 $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE ); 409 } 410 } 411 412 // Limit the bit depth of resized images to 8 bits per channel. 413 if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) { 414 if ( 8 < $this->image->getImageDepth() ) { 415 $this->image->setImageDepth( 8 ); 416 } 417 } 418 419 if ( is_callable( array( $this->image, 'setInterlaceScheme' ) ) && defined( 'Imagick::INTERLACE_NO' ) ) { 420 $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); 421 } 422 } catch ( Exception $e ) { 423 return new WP_Error( 'image_resize_error', $e->getMessage() ); 424 } 425 } 426 427 /** 428 * Create multiple smaller images from a single source. 429 * 430 * Attempts to create all sub-sizes and returns the meta data at the end. This 431 * may result in the server running out of resources. When it fails there may be few 432 * "orphaned" images left over as the meta data is never returned and saved. 433 * 434 * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates 435 * the new images one at a time and allows for the meta data to be saved after 436 * each new image is created. 437 * 438 * @since 3.5.0 439 * 440 * @param array $sizes { 441 * An array of image size data arrays. 442 * 443 * Either a height or width must be provided. 444 * If one of the two is set to null, the resize will 445 * maintain aspect ratio according to the provided dimension. 446 * 447 * @type array ...$0 { 448 * Array of height, width values, and whether to crop. 449 * 450 * @type int $width Image width. Optional if `$height` is specified. 451 * @type int $height Image height. Optional if `$width` is specified. 452 * @type bool $crop Optional. Whether to crop the image. Default false. 453 * } 454 * } 455 * @return array An array of resized images' metadata by size. 456 */ 457 public function multi_resize( $sizes ) { 458 $metadata = array(); 459 460 foreach ( $sizes as $size => $size_data ) { 461 $meta = $this->make_subsize( $size_data ); 462 463 if ( ! is_wp_error( $meta ) ) { 464 $metadata[ $size ] = $meta; 465 } 466 } 467 468 return $metadata; 469 } 470 471 /** 472 * Create an image sub-size and return the image meta data value for it. 473 * 474 * @since 5.3.0 475 * 476 * @param array $size_data { 477 * Array of size data. 478 * 479 * @type int $width The maximum width in pixels. 480 * @type int $height The maximum height in pixels. 481 * @type bool $crop Whether to crop the image to exact dimensions. 482 * } 483 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta, 484 * WP_Error object on error. 485 */ 486 public function make_subsize( $size_data ) { 487 if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { 488 return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) ); 489 } 490 491 $orig_size = $this->size; 492 $orig_image = $this->image->getImage(); 493 494 if ( ! isset( $size_data['width'] ) ) { 495 $size_data['width'] = null; 496 } 497 498 if ( ! isset( $size_data['height'] ) ) { 499 $size_data['height'] = null; 500 } 501 502 if ( ! isset( $size_data['crop'] ) ) { 503 $size_data['crop'] = false; 504 } 505 506 $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] ); 507 508 if ( is_wp_error( $resized ) ) { 509 $saved = $resized; 510 } else { 511 $saved = $this->_save( $this->image ); 512 513 $this->image->clear(); 514 $this->image->destroy(); 515 $this->image = null; 516 } 517 518 $this->size = $orig_size; 519 $this->image = $orig_image; 520 521 if ( ! is_wp_error( $saved ) ) { 522 unset( $saved['path'] ); 523 } 524 525 return $saved; 526 } 527 528 /** 529 * Crops Image. 530 * 531 * @since 3.5.0 532 * 533 * @param int $src_x The start x position to crop from. 534 * @param int $src_y The start y position to crop from. 535 * @param int $src_w The width to crop. 536 * @param int $src_h The height to crop. 537 * @param int $dst_w Optional. The destination width. 538 * @param int $dst_h Optional. The destination height. 539 * @param bool $src_abs Optional. If the source crop points are absolute. 540 * @return true|WP_Error 541 */ 542 public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { 543 if ( $src_abs ) { 544 $src_w -= $src_x; 545 $src_h -= $src_y; 546 } 547 548 try { 549 $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); 550 $this->image->setImagePage( $src_w, $src_h, 0, 0 ); 551 552 if ( $dst_w || $dst_h ) { 553 // If destination width/height isn't specified, 554 // use same as width/height from source. 555 if ( ! $dst_w ) { 556 $dst_w = $src_w; 557 } 558 if ( ! $dst_h ) { 559 $dst_h = $src_h; 560 } 561 562 $thumb_result = $this->thumbnail_image( $dst_w, $dst_h ); 563 if ( is_wp_error( $thumb_result ) ) { 564 return $thumb_result; 565 } 566 567 return $this->update_size(); 568 } 569 } catch ( Exception $e ) { 570 return new WP_Error( 'image_crop_error', $e->getMessage() ); 571 } 572 573 return $this->update_size(); 574 } 575 576 /** 577 * Rotates current image counter-clockwise by $angle. 578 * 579 * @since 3.5.0 580 * 581 * @param float $angle 582 * @return true|WP_Error 583 */ 584 public function rotate( $angle ) { 585 /** 586 * $angle is 360-$angle because Imagick rotates clockwise 587 * (GD rotates counter-clockwise) 588 */ 589 try { 590 $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle ); 591 592 // Normalize EXIF orientation data so that display is consistent across devices. 593 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 594 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 595 } 596 597 // Since this changes the dimensions of the image, update the size. 598 $result = $this->update_size(); 599 if ( is_wp_error( $result ) ) { 600 return $result; 601 } 602 603 $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 ); 604 } catch ( Exception $e ) { 605 return new WP_Error( 'image_rotate_error', $e->getMessage() ); 606 } 607 608 return true; 609 } 610 611 /** 612 * Flips current image. 613 * 614 * @since 3.5.0 615 * 616 * @param bool $horz Flip along Horizontal Axis 617 * @param bool $vert Flip along Vertical Axis 618 * @return true|WP_Error 619 */ 620 public function flip( $horz, $vert ) { 621 try { 622 if ( $horz ) { 623 $this->image->flipImage(); 624 } 625 626 if ( $vert ) { 627 $this->image->flopImage(); 628 } 629 630 // Normalize EXIF orientation data so that display is consistent across devices. 631 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 632 $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT ); 633 } 634 } catch ( Exception $e ) { 635 return new WP_Error( 'image_flip_error', $e->getMessage() ); 636 } 637 638 return true; 639 } 640 641 /** 642 * Check if a JPEG image has EXIF Orientation tag and rotate it if needed. 643 * 644 * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only 645 * if EXIF Orientation can be reset afterwards. 646 * 647 * @since 5.3.0 648 * 649 * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation. 650 * WP_Error if error while rotating. 651 */ 652 public function maybe_exif_rotate() { 653 if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) { 654 return parent::maybe_exif_rotate(); 655 } else { 656 return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) ); 657 } 658 } 659 660 /** 661 * Saves current image to file. 662 * 663 * @since 3.5.0 664 * 665 * @param string $destfilename Optional. Destination filename. Default null. 666 * @param string $mime_type Optional. The mime-type. Default null. 667 * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string} 668 */ 669 public function save( $destfilename = null, $mime_type = null ) { 670 $saved = $this->_save( $this->image, $destfilename, $mime_type ); 671 672 if ( ! is_wp_error( $saved ) ) { 673 $this->file = $saved['path']; 674 $this->mime_type = $saved['mime-type']; 675 676 try { 677 $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) ); 678 } catch ( Exception $e ) { 679 return new WP_Error( 'image_save_error', $e->getMessage(), $this->file ); 680 } 681 } 682 683 return $saved; 684 } 685 686 /** 687 * @param Imagick $image 688 * @param string $filename 689 * @param string $mime_type 690 * @return array|WP_Error 691 */ 692 protected function _save( $image, $filename = null, $mime_type = null ) { 693 list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type ); 694 695 if ( ! $filename ) { 696 $filename = $this->generate_filename( null, null, $extension ); 697 } 698 699 try { 700 // Store initial format. 701 $orig_format = $this->image->getImageFormat(); 702 703 $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); 704 } catch ( Exception $e ) { 705 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 706 } 707 708 $write_image_result = $this->write_image( $this->image, $filename ); 709 if ( is_wp_error( $write_image_result ) ) { 710 return $write_image_result; 711 } 712 713 try { 714 // Reset original format. 715 $this->image->setImageFormat( $orig_format ); 716 } catch ( Exception $e ) { 717 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 718 } 719 720 // Set correct file permissions. 721 $stat = stat( dirname( $filename ) ); 722 $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. 723 chmod( $filename, $perms ); 724 725 return array( 726 'path' => $filename, 727 /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ 728 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ), 729 'width' => $this->size['width'], 730 'height' => $this->size['height'], 731 'mime-type' => $mime_type, 732 'filesize' => wp_filesize( $filename ), 733 ); 734 } 735 736 /** 737 * Writes an image to a file or stream. 738 * 739 * @since 5.6.0 740 * 741 * @param Imagick $image 742 * @param string $filename The destination filename or stream URL. 743 * @return true|WP_Error 744 */ 745 private function write_image( $image, $filename ) { 746 if ( wp_is_stream( $filename ) ) { 747 /* 748 * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. 749 * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php 750 */ 751 if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { 752 return new WP_Error( 753 'image_save_error', 754 sprintf( 755 /* translators: %s: PHP function name. */ 756 __( '%s failed while writing image to stream.' ), 757 '<code>file_put_contents()</code>' 758 ), 759 $filename 760 ); 761 } else { 762 return true; 763 } 764 } else { 765 $dirname = dirname( $filename ); 766 767 if ( ! wp_mkdir_p( $dirname ) ) { 768 return new WP_Error( 769 'image_save_error', 770 sprintf( 771 /* translators: %s: Directory path. */ 772 __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), 773 esc_html( $dirname ) 774 ) 775 ); 776 } 777 778 try { 779 return $image->writeImage( $filename ); 780 } catch ( Exception $e ) { 781 return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); 782 } 783 } 784 } 785 786 /** 787 * Streams current image to browser. 788 * 789 * @since 3.5.0 790 * 791 * @param string $mime_type The mime type of the image. 792 * @return true|WP_Error True on success, WP_Error object on failure. 793 */ 794 public function stream( $mime_type = null ) { 795 list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); 796 797 try { 798 // Temporarily change format for stream. 799 $this->image->setImageFormat( strtoupper( $extension ) ); 800 801 // Output stream of image content. 802 header( "Content-Type: $mime_type" ); 803 print $this->image->getImageBlob(); 804 805 // Reset image to original format. 806 $this->image->setImageFormat( $this->get_extension( $this->mime_type ) ); 807 } catch ( Exception $e ) { 808 return new WP_Error( 'image_stream_error', $e->getMessage() ); 809 } 810 811 return true; 812 } 813 814 /** 815 * Strips all image meta except color profiles from an image. 816 * 817 * @since 4.5.0 818 * 819 * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error. 820 */ 821 protected function strip_meta() { 822 823 if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { 824 return new WP_Error( 825 'image_strip_meta_error', 826 sprintf( 827 /* translators: %s: ImageMagick method name. */ 828 __( '%s is required to strip image meta.' ), 829 '<code>Imagick::getImageProfiles()</code>' 830 ) 831 ); 832 } 833 834 if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { 835 return new WP_Error( 836 'image_strip_meta_error', 837 sprintf( 838 /* translators: %s: ImageMagick method name. */ 839 __( '%s is required to strip image meta.' ), 840 '<code>Imagick::removeImageProfile()</code>' 841 ) 842 ); 843 } 844 845 /* 846 * Protect a few profiles from being stripped for the following reasons: 847 * 848 * - icc: Color profile information 849 * - icm: Color profile information 850 * - iptc: Copyright data 851 * - exif: Orientation data 852 * - xmp: Rights usage data 853 */ 854 $protected_profiles = array( 855 'icc', 856 'icm', 857 'iptc', 858 'exif', 859 'xmp', 860 ); 861 862 try { 863 // Strip profiles. 864 foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) { 865 if ( ! in_array( $key, $protected_profiles, true ) ) { 866 $this->image->removeImageProfile( $key ); 867 } 868 } 869 } catch ( Exception $e ) { 870 return new WP_Error( 'image_strip_meta_error', $e->getMessage() ); 871 } 872 873 return true; 874 } 875 876 /** 877 * Sets up Imagick for PDF processing. 878 * Increases rendering DPI and only loads first page. 879 * 880 * @since 4.7.0 881 * 882 * @return string|WP_Error File to load or WP_Error on failure. 883 */ 884 protected function pdf_setup() { 885 try { 886 // By default, PDFs are rendered in a very low resolution. 887 // We want the thumbnail to be readable, so increase the rendering DPI. 888 $this->image->setResolution( 128, 128 ); 889 890 // Only load the first page. 891 return $this->file . '[0]'; 892 } catch ( Exception $e ) { 893 return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file ); 894 } 895 } 896 897 /** 898 * Load the image produced by Ghostscript. 899 * 900 * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files 901 * when `use-cropbox` is set. 902 * 903 * @since 5.6.0 904 * 905 * @return true|WP_Error 906 */ 907 protected function pdf_load_source() { 908 $filename = $this->pdf_setup(); 909 910 if ( is_wp_error( $filename ) ) { 911 return $filename; 912 } 913 914 try { 915 // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped 916 // area (resulting in unnecessary whitespace) unless the following option is set. 917 $this->image->setOption( 'pdf:use-cropbox', true ); 918 919 // Reading image after Imagick instantiation because `setResolution` 920 // only applies correctly before the image is read. 921 $this->image->readImage( $filename ); 922 } catch ( Exception $e ) { 923 // Attempt to run `gs` without the `use-cropbox` option. See #48853. 924 $this->image->setOption( 'pdf:use-cropbox', false ); 925 926 $this->image->readImage( $filename ); 927 } 928 929 return true; 930 } 931 932 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Dec 22 01:00:02 2024 | Cross-referenced by PHPXref 0.7.1 |