[ Index ]

PHP Cross Reference of WordPress

title

Body

[close]

/wp-includes/ -> class-wp-image-editor-imagick.php (source)

   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  }


Generated: Thu Nov 21 01:00:03 2024 Cross-referenced by PHPXref 0.7.1