[ 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 doesn&#8217;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 bool|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 bool|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 bool|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                  /* translators: %s: PHP function name. */
 739                  return new WP_Error( 'image_save_error', sprintf( __( '%s failed while writing image to stream.' ), '<code>file_put_contents()</code>' ), $filename );
 740              } else {
 741                  return true;
 742              }
 743          } else {
 744              try {
 745                  return $image->writeImage( $filename );
 746              } catch ( Exception $e ) {
 747                  return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
 748              }
 749          }
 750      }
 751  
 752      /**
 753       * Streams current image to browser.
 754       *
 755       * @since 3.5.0
 756       *
 757       * @param string $mime_type The mime type of the image.
 758       * @return bool|WP_Error True on success, WP_Error object on failure.
 759       */
 760  	public function stream( $mime_type = null ) {
 761          list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
 762  
 763          try {
 764              // Temporarily change format for stream.
 765              $this->image->setImageFormat( strtoupper( $extension ) );
 766  
 767              // Output stream of image content.
 768              header( "Content-Type: $mime_type" );
 769              print $this->image->getImageBlob();
 770  
 771              // Reset image to original format.
 772              $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
 773          } catch ( Exception $e ) {
 774              return new WP_Error( 'image_stream_error', $e->getMessage() );
 775          }
 776  
 777          return true;
 778      }
 779  
 780      /**
 781       * Strips all image meta except color profiles from an image.
 782       *
 783       * @since 4.5.0
 784       *
 785       * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
 786       */
 787  	protected function strip_meta() {
 788  
 789          if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
 790              /* translators: %s: ImageMagick method name. */
 791              return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::getImageProfiles()</code>' ) );
 792          }
 793  
 794          if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
 795              /* translators: %s: ImageMagick method name. */
 796              return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::removeImageProfile()</code>' ) );
 797          }
 798  
 799          /*
 800           * Protect a few profiles from being stripped for the following reasons:
 801           *
 802           * - icc:  Color profile information
 803           * - icm:  Color profile information
 804           * - iptc: Copyright data
 805           * - exif: Orientation data
 806           * - xmp:  Rights usage data
 807           */
 808          $protected_profiles = array(
 809              'icc',
 810              'icm',
 811              'iptc',
 812              'exif',
 813              'xmp',
 814          );
 815  
 816          try {
 817              // Strip profiles.
 818              foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
 819                  if ( ! in_array( $key, $protected_profiles, true ) ) {
 820                      $this->image->removeImageProfile( $key );
 821                  }
 822              }
 823          } catch ( Exception $e ) {
 824              return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
 825          }
 826  
 827          return true;
 828      }
 829  
 830      /**
 831       * Sets up Imagick for PDF processing.
 832       * Increases rendering DPI and only loads first page.
 833       *
 834       * @since 4.7.0
 835       *
 836       * @return string|WP_Error File to load or WP_Error on failure.
 837       */
 838  	protected function pdf_setup() {
 839          try {
 840              // By default, PDFs are rendered in a very low resolution.
 841              // We want the thumbnail to be readable, so increase the rendering DPI.
 842              $this->image->setResolution( 128, 128 );
 843  
 844              // Only load the first page.
 845              return $this->file . '[0]';
 846          } catch ( Exception $e ) {
 847              return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
 848          }
 849      }
 850  
 851      /**
 852       * Load the image produced by Ghostscript.
 853       *
 854       * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files
 855       * when `use-cropbox` is set.
 856       *
 857       * @since 5.6.0
 858       *
 859       * @return true|WP_error
 860       */
 861  	protected function pdf_load_source() {
 862          $filename = $this->pdf_setup();
 863  
 864          if ( is_wp_error( $filename ) ) {
 865              return $filename;
 866          }
 867  
 868          try {
 869              // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
 870              // area (resulting in unnecessary whitespace) unless the following option is set.
 871              $this->image->setOption( 'pdf:use-cropbox', true );
 872  
 873              // Reading image after Imagick instantiation because `setResolution`
 874              // only applies correctly before the image is read.
 875              $this->image->readImage( $filename );
 876          } catch ( Exception $e ) {
 877              // Attempt to run `gs` without the `use-cropbox` option. See #48853.
 878              $this->image->setOption( 'pdf:use-cropbox', false );
 879  
 880              $this->image->readImage( $filename );
 881          }
 882  
 883          return true;
 884      }
 885  
 886  }


Generated: Tue Oct 27 01:00:08 2020 Cross-referenced by PHPXref 0.7.1