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