[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

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      /**
  25       * Temporarily stores stream image data while processing internally.
  26       *
  27       * @see self::pdf_load_source()
  28       *
  29       * @since 7.0.4
  30       *
  31       * @var string|null
  32       */
  33      private $stream_file_data = null;
  34  
  35      /**
  36       * Temporarily stores the parsed given name for an image while processing internally.
  37       *
  38       * @see self::pdf_load_source()
  39       *
  40       * @since 7.0.4
  41       *
  42       * @var string|null
  43       */
  44      private $image_given_name = null;
  45  
  46  	public function __destruct() {
  47          if ( $this->image instanceof Imagick ) {
  48              // We don't need the original in memory anymore.
  49              $this->image->clear();
  50              $this->image->destroy();
  51          }
  52      }
  53  
  54      /**
  55       * Checks to see if current environment supports Imagick.
  56       *
  57       * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
  58       * method can be called statically.
  59       *
  60       * @since 3.5.0
  61       *
  62       * @param array $args
  63       * @return bool
  64       */
  65  	public static function test( $args = array() ) {
  66  
  67          // First, test Imagick's extension and classes.
  68          if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) {
  69              return false;
  70          }
  71  
  72          if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) {
  73              return false;
  74          }
  75  
  76          $required_methods = array(
  77              'clear',
  78              'destroy',
  79              'valid',
  80              'getimage',
  81              'writeimage',
  82              'getimageblob',
  83              'getimagegeometry',
  84              'getimageformat',
  85              'setimageformat',
  86              'setimagecompression',
  87              'setimagecompressionquality',
  88              'setimagepage',
  89              'setoption',
  90              'scaleimage',
  91              'cropimage',
  92              'rotateimage',
  93              'flipimage',
  94              'flopimage',
  95              'readimage',
  96              'readimageblob',
  97          );
  98  
  99          // Now, test for deep requirements within Imagick.
 100          if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
 101              return false;
 102          }
 103  
 104          $class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) );
 105          if ( array_diff( $required_methods, $class_methods ) ) {
 106              return false;
 107          }
 108  
 109          return true;
 110      }
 111  
 112      /**
 113       * Checks to see if editor supports the mime-type specified.
 114       *
 115       * @since 3.5.0
 116       *
 117       * @param string $mime_type
 118       * @return bool
 119       */
 120  	public static function supports_mime_type( $mime_type ) {
 121          $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
 122  
 123          if ( ! $imagick_extension ) {
 124              return false;
 125          }
 126  
 127          /*
 128           * setIteratorIndex is optional unless mime is an animated format.
 129           * Here, we just say no if you are missing it and aren't loading a jpeg.
 130           */
 131          if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
 132                  return false;
 133          }
 134  
 135          try {
 136              // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
 137              return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
 138          } catch ( Exception $e ) {
 139              return false;
 140          }
 141      }
 142  
 143      /**
 144       * Loads image from $this->file into new Imagick Object.
 145       *
 146       * @since 3.5.0
 147       *
 148       * @return true|WP_Error True if loaded; WP_Error on failure.
 149       */
 150  	public function load() {
 151          if ( $this->image instanceof Imagick ) {
 152              return true;
 153          }
 154  
 155          $is_stream = wp_is_stream( $this->file );
 156          $is_file   = ! $is_stream && is_file( $this->file );
 157  
 158          // Only allow loading files or streams.
 159          if ( ! $is_file && ! $is_stream ) {
 160              return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file );
 161          }
 162  
 163          // Establish the provided filename based on the kind of resource being loaded.
 164          $given_filename = $this->file;
 165          if ( 0 === strncasecmp( $given_filename, 'file://', 7 ) ) {
 166              $given_filename = basename( substr( $given_filename, 7 ) ); // 7 is the strlen of 'file://'.
 167          } elseif ( 1 === preg_match( '~^https?://~i', $this->file ) ) {
 168              /*
 169               * For URLs, it will be the final path segment.
 170               *
 171               * Example:
 172               *
 173               *     https://wordpress.org/i/happy.png?size=40px
 174               *                             ╰───────╯
 175               *                                this is the given filename
 176               *
 177               * If the stream returns a `Content-Disposition` header it would
 178               * provide an alternative name, but this is used as a reasonable
 179               * proxy to avoid adding the additional complexity of reading and
 180               * parsing the returned HTTP headers.
 181               */
 182              $url_path = wp_parse_url( $this->file, PHP_URL_PATH );
 183  
 184              // This URL can not be parsed, so it is not a valid image resource.
 185              if ( false === $url_path ) {
 186                  return new WP_Error( 'error_loading_image', __( 'File is not an image.' ), $this->file );
 187              }
 188  
 189              /**
 190               * The URL has an empty path, so continue with an empty string.
 191               *
 192               * This is the case with a URL such as `https://example.com?file_id=123`
 193               */
 194              if ( null === $url_path ) {
 195                  $url_path = '';
 196              }
 197  
 198              $last_path_at   = strrpos( $url_path, '/' );
 199              $given_filename = is_int( $last_path_at ) ? substr( $url_path, $last_path_at + 1 ) : $url_path;
 200              $given_filename = rawurldecode( $given_filename );
 201          }
 202  
 203          /*
 204           * Strip off any potential `Imagick` format specifiers.
 205           *
 206           * If a real file exists with the identified format specifier, then
 207           * `Imagick` may not treat it as a format, but WordPress will reject
 208           * it anyway to avoid adding more complexity into this detection.
 209           *
 210           * `Imagick` reads only the first `FORMAT:` specifier on a name, but
 211           * stripping a segment would promote a second specifier to the front
 212           * of the name handed to `Imagick`, which would then honor it.
 213           *
 214           * Loop to capture all format specifiers for comparison.
 215           *
 216           * Exclude Windows drive-letter prefixes from here.
 217           */
 218          $imagick_formats = array();
 219          while (
 220              false !== ( $format_ends_at = strpos( $given_filename, ':' ) ) &&
 221              1 !== preg_match( '~^[a-z]:~i', $given_filename )
 222          ) {
 223              $imagick_formats[] = strtoupper( substr( $given_filename, 0, $format_ends_at ) );
 224              $given_filename    = substr( $given_filename, $format_ends_at + 1 );
 225          }
 226  
 227          $file_extension = strtolower( pathinfo( $given_filename, PATHINFO_EXTENSION ) );
 228  
 229          /*
 230           * Even though Imagick uses less PHP memory than GD, set higher limit
 231           * for users that have low PHP.ini limits.
 232           */
 233          wp_raise_memory_limit( 'image' );
 234  
 235          /**
 236           * Read the resource header for MIME sniffing.
 237           *
 238           * For files, which will be passed into Imagick by their file names, avoid
 239           * eagerly loading the entire contents into PHP memory. For streams, however,
 240           * it’s more important to avoid validating a separate copy of the file data
 241           * than is later fetched by Imagick, so go ahead and load the entire payload,
 242           * then pass it to Imagick as the data blob itself.
 243           *
 244           * @link https://mimesniff.spec.whatwg.org/#reading-the-resource-header
 245           */
 246          try {
 247              if ( $is_file ) {
 248                  $file_data = file_get_contents( $this->file, false, null, 0, 1445 );
 249              } else {
 250                  $file_data = file_get_contents( $this->file );
 251              }
 252          } catch ( Exception $e ) {
 253              $file_data = false;
 254          }
 255          if ( false === $file_data ) {
 256              return new WP_Error( 'error_loading_image', __( 'File does not exist?' ), $this->file );
 257          }
 258  
 259          $pdf_extensions = array(
 260              'ai',
 261              'epdf',
 262              'pdf',
 263              'pdfa',
 264              'pocketmod',
 265          );
 266  
 267          // Reject files claiming to be PDFs which lack the required signature.
 268          $has_pdf_extension = in_array( $file_extension, $pdf_extensions, true );
 269          $has_pdf_signature = str_starts_with( $file_data, '%PDF-' );
 270          if ( $has_pdf_extension && ! $has_pdf_signature ) {
 271              return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
 272          }
 273  
 274          $ps_formats = array(
 275              'DPS',
 276              'EPI',
 277              'EPS',
 278              'EPSF',
 279              'EPSI',
 280              'PS',
 281              'WPG',
 282          );
 283  
 284          $ps_extensions = array(
 285              'dps',
 286              'epi',
 287              'eps',
 288              'eps2',
 289              'eps3',
 290              'epsf',
 291              'epsi',
 292              'ept',
 293              'ept2',
 294              'ept3',
 295              'ps',
 296              'ps2',
 297              'ps3',
 298              'wpg',
 299          );
 300  
 301          // Reject files which Imagick will parse as PostScript.
 302          if (
 303              array() !== array_intersect( $imagick_formats, $ps_formats ) ||
 304              in_array( $file_extension, $ps_extensions, true ) ||
 305              str_starts_with( $file_data, '%!' ) ||
 306              str_starts_with( $file_data, "\x04%!" ) ||
 307              str_starts_with( $file_data, "\xC5\xD0\xD3\xC6" ) ||
 308              str_starts_with( $file_data, "\xFFWPC" )
 309          ) {
 310              return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
 311          }
 312  
 313          $compressed_extensions = array(
 314              'gz',
 315              'bz2',
 316              'svgz',
 317              'z',
 318              'wmz',
 319          );
 320  
 321          /*
 322           * Reject compressed archives that Imagick will transparently decompress.
 323           * Unfortunately this rejects `.svgz` because there’s no intermediate step
 324           * in the loading process. `Imagick` would decompress the file, then look
 325           * to see what kind of content was decompressed instead of asserting SVG.
 326           */
 327          if (
 328              in_array( $file_extension, $compressed_extensions, true ) ||
 329              str_starts_with( $file_data, "\x1F\x8B\x08" ) || // gzip
 330              str_starts_with( $file_data, 'BZh' ) || // bzip2
 331              str_starts_with( $file_data, "\x1F\x9D" ) // compress
 332          ) {
 333              return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
 334          }
 335  
 336          try {
 337              $this->image = new Imagick();
 338  
 339              if ( $has_pdf_signature ) {
 340                  /*
 341                   * Load these values for use in the helper method without forcing a change
 342                   * of its expected arguments, but then free them after calling to prevent
 343                   * keeping them around in memory and bloating the app.
 344                   */
 345                  $this->stream_file_data = $is_stream ? $file_data : null;
 346                  $this->image_given_name = $given_filename;
 347                  $pdf_loaded             = $this->pdf_load_source();
 348                  $this->stream_file_data = null;
 349                  $this->image_given_name = null;
 350  
 351                  if ( is_wp_error( $pdf_loaded ) ) {
 352                      return $pdf_loaded;
 353                  }
 354              } else {
 355                  if ( $is_stream ) {
 356                      $this->image->readImageBlob( $file_data, $given_filename );
 357                  } else {
 358                      $this->image->readImage( $this->file );
 359                  }
 360              }
 361  
 362              if ( ! $this->image->valid() ) {
 363                  return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
 364              }
 365  
 366              // Select the first frame to handle animated images properly.
 367              if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
 368                  $this->image->setIteratorIndex( 0 );
 369              }
 370  
 371              if ( $has_pdf_signature ) {
 372                  $this->remove_pdf_alpha_channel();
 373              }
 374  
 375              $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
 376          } catch ( Exception $e ) {
 377              return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
 378          }
 379  
 380          $updated_size = $this->update_size();
 381  
 382          if ( is_wp_error( $updated_size ) ) {
 383              return $updated_size;
 384          }
 385  
 386          return $this->set_quality();
 387      }
 388  
 389      /**
 390       * Sets Image Compression quality on a 1-100% scale.
 391       *
 392       * @since 3.5.0
 393       * @since 6.8.0 The `$dims` parameter was added.
 394       *
 395       * @param int   $quality Compression Quality. Range: [1,100]
 396       * @param array $dims    Optional. Image dimensions array with 'width' and 'height' keys.
 397       * @return true|WP_Error True if set successfully; WP_Error on failure.
 398       */
 399  	public function set_quality( $quality = null, $dims = array() ) {
 400          $quality_result = parent::set_quality( $quality, $dims );
 401          if ( is_wp_error( $quality_result ) ) {
 402              return $quality_result;
 403          } else {
 404              $quality = $this->get_quality();
 405          }
 406  
 407          try {
 408              switch ( $this->mime_type ) {
 409                  case 'image/jpeg':
 410                      $this->image->setImageCompressionQuality( $quality );
 411                      $this->image->setCompressionQuality( $quality );
 412                      $this->image->setImageCompression( imagick::COMPRESSION_JPEG );
 413                      break;
 414                  case 'image/webp':
 415                      $webp_info = wp_get_webp_info( $this->file );
 416  
 417                      if ( 'lossless' === $webp_info['type'] ) {
 418                          // Use WebP lossless settings.
 419                          $this->image->setImageCompressionQuality( 100 );
 420                          $this->image->setCompressionQuality( 100 );
 421                          $this->image->setOption( 'webp:lossless', 'true' );
 422                          parent::set_quality( 100 );
 423                      } else {
 424                          $this->image->setImageCompressionQuality( $quality );
 425                          $this->image->setCompressionQuality( $quality );
 426                      }
 427                      break;
 428                  case 'image/avif':
 429                      // Set the AVIF encoder to work faster, with minimal impact on image size.
 430                      $this->image->setOption( 'heic:speed', 7 );
 431                      $this->image->setImageCompressionQuality( $quality );
 432                      $this->image->setCompressionQuality( $quality );
 433                      break;
 434                  default:
 435                      $this->image->setImageCompressionQuality( $quality );
 436                      $this->image->setCompressionQuality( $quality );
 437              }
 438          } catch ( Exception $e ) {
 439              return new WP_Error( 'image_quality_error', $e->getMessage() );
 440          }
 441          return true;
 442      }
 443  
 444  
 445      /**
 446       * Sets or updates current image size.
 447       *
 448       * @since 3.5.0
 449       *
 450       * @param int|null $width  Image width.
 451       * @param int|null $height Image height.
 452       * @return true|WP_Error
 453       */
 454  	protected function update_size( $width = null, $height = null ) {
 455          $size = null;
 456  
 457          if ( ! $width || ! $height ) {
 458              try {
 459                  $size = $this->image->getImageGeometry();
 460              } catch ( Exception $e ) {
 461                  return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
 462              }
 463          }
 464  
 465          if ( ! $width ) {
 466              $width = $size['width'];
 467          }
 468  
 469          if ( ! $height ) {
 470              $height = $size['height'];
 471          }
 472  
 473          /*
 474           * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF and HEIC images
 475           * are properly sized without affecting previous `getImageGeometry` behavior.
 476           */
 477          if ( ( ! $width || ! $height ) && ( 'image/avif' === $this->mime_type || wp_is_heic_image_mime_type( $this->mime_type ) ) ) {
 478              $size   = wp_getimagesize( $this->file );
 479              $width  = $size[0];
 480              $height = $size[1];
 481          }
 482  
 483          return parent::update_size( $width, $height );
 484      }
 485  
 486      /**
 487       * Sets Imagick time limit.
 488       *
 489       * Depending on configuration, Imagick processing may take time.
 490       *
 491       * Multiple problems exist if PHP times out before ImageMagick completed:
 492       * 1. Temporary files aren't cleaned by ImageMagick garbage collection.
 493       * 2. No clear error is provided.
 494       * 3. The cause of such timeout can be hard to pinpoint.
 495       *
 496       * This function, which is expected to be run before heavy image routines, resolves
 497       * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set.
 498       *
 499       * However seems it introduces more problems than it fixes,
 500       * see https://core.trac.wordpress.org/ticket/58202.
 501       *
 502       * Note:
 503       *  - Imagick resource exhaustion does not issue catchable exceptions (yet).
 504       *    See https://github.com/Imagick/imagick/issues/333.
 505       *  - The resource limit is not saved/restored. It applies to subsequent
 506       *    image operations within the time of the HTTP request.
 507       *
 508       * @since 6.2.0
 509       * @deprecated 6.3.0 No longer used in core.
 510       *
 511       * @return int|null The new limit on success, null on failure.
 512       */
 513  	public static function set_imagick_time_limit() {
 514          _deprecated_function( __METHOD__, '6.3.0' );
 515  
 516          if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) {
 517              return null;
 518          }
 519  
 520          // Returns PHP_FLOAT_MAX if unset.
 521          $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME );
 522  
 523          // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX.
 524          $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout;
 525  
 526          $php_timeout = (int) ini_get( 'max_execution_time' );
 527  
 528          if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) {
 529              $limit = (float) 0.8 * $php_timeout;
 530              Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit );
 531  
 532              return $limit;
 533          }
 534      }
 535  
 536      /**
 537       * Resizes current image.
 538       *
 539       * At minimum, either a height or width must be provided.
 540       * If one of the two is set to null, the resize will
 541       * maintain aspect ratio according to the provided dimension.
 542       *
 543       * @since 3.5.0
 544       *
 545       * @param int|null   $max_w Image width.
 546       * @param int|null   $max_h Image height.
 547       * @param bool|array $crop  {
 548       *     Optional. Image cropping behavior. If false, the image will be scaled (default).
 549       *     If true, image will be cropped to the specified dimensions using center positions.
 550       *     If an array, the image will be cropped using the array to specify the crop location:
 551       *
 552       *     @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
 553       *     @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
 554       * }
 555       * @return true|WP_Error
 556       */
 557  	public function resize( $max_w, $max_h, $crop = false ) {
 558          if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) {
 559              return true;
 560          }
 561  
 562          $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
 563          if ( ! $dims ) {
 564              return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
 565          }
 566  
 567          list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
 568  
 569          if ( $crop ) {
 570              return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
 571          }
 572  
 573          $this->set_quality(
 574              null,
 575              array(
 576                  'width'  => $dst_w,
 577                  'height' => $dst_h,
 578              )
 579          );
 580  
 581          // Execute the resize.
 582          $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
 583          if ( is_wp_error( $thumb_result ) ) {
 584              return $thumb_result;
 585          }
 586  
 587          return $this->update_size( $dst_w, $dst_h );
 588      }
 589  
 590      /**
 591       * Efficiently resize the current image
 592       *
 593       * This is a WordPress specific implementation of Imagick::thumbnailImage(),
 594       * which resizes an image to given dimensions and removes any associated profiles.
 595       *
 596       * @since 4.5.0
 597       *
 598       * @param int    $dst_w       The destination width.
 599       * @param int    $dst_h       The destination height.
 600       * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
 601       * @param bool   $strip_meta  Optional. Strip all profiles, excluding color profiles, from the image. Default true.
 602       * @return void|WP_Error
 603       */
 604  	protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
 605          $allowed_filters = array(
 606              'FILTER_POINT',
 607              'FILTER_BOX',
 608              'FILTER_TRIANGLE',
 609              'FILTER_HERMITE',
 610              'FILTER_HANNING',
 611              'FILTER_HAMMING',
 612              'FILTER_BLACKMAN',
 613              'FILTER_GAUSSIAN',
 614              'FILTER_QUADRATIC',
 615              'FILTER_CUBIC',
 616              'FILTER_CATROM',
 617              'FILTER_MITCHELL',
 618              'FILTER_LANCZOS',
 619              'FILTER_BESSEL',
 620              'FILTER_SINC',
 621          );
 622  
 623          /**
 624           * Set the filter value if '$filter_name' name is in the allowed list and the related
 625           * Imagick constant is defined or fall back to the default filter.
 626           */
 627          if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
 628              $filter = constant( 'Imagick::' . $filter_name );
 629          } else {
 630              $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
 631          }
 632  
 633          /**
 634           * Filters whether to strip metadata from images when they're resized.
 635           *
 636           * This filter only applies when resizing using the Imagick editor since GD
 637           * always strips profiles by default.
 638           *
 639           * @since 4.5.0
 640           *
 641           * @param bool $strip_meta Whether to strip image metadata during resizing. Default true.
 642           */
 643          if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {
 644              $this->strip_meta(); // Fail silently if not supported.
 645          }
 646  
 647          try {
 648              /**
 649               * Special handling for certain types of PNG images:
 650               * 1. For PNG images, we need to specify compression settings and remove unneeded chunks.
 651               * 2. For indexed PNG images, the number of colors must not exceed 256.
 652               * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved.
 653               * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving
 654               * the image using ImageMagick's 'png8' format,  because that supports only binary (1 bit) transparency.
 655               *
 656               * For #4 we want to check whether the image has a 1-bit alpha channel before resizing,  because resizing
 657               * may cause the number of alpha values to multiply due to antialiasing. If the original image had only a
 658               * 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images.
 659               *
 660               * Perform all the necessary checks before resizing the image and store the results in variables for later use.
 661               */
 662              $is_png                                      = false;
 663              $is_indexed_png                              = false;
 664              $is_indexed_png_with_alpha_channel           = false;
 665              $is_indexed_png_with_true_alpha_transparency = false;
 666  
 667              if ( 'image/png' === $this->mime_type ) {
 668                  $is_png = true;
 669  
 670                  if (
 671                      is_callable( array( $this->image, 'getImageProperty' ) )
 672                      && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
 673                  ) {
 674                      $is_indexed_png = true;
 675  
 676                      if (
 677                          is_callable( array( $this->image, 'getImageAlphaChannel' ) )
 678                          && $this->image->getImageAlphaChannel()
 679                      ) {
 680                          $is_indexed_png_with_alpha_channel = true;
 681  
 682                          if (
 683                              is_callable( array( $this->image, 'getImageChannelDepth' ) )
 684                              && defined( 'Imagick::CHANNEL_ALPHA' )
 685                              && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA )
 686                          ) {
 687                              $is_indexed_png_with_true_alpha_transparency = true;
 688                          }
 689                      }
 690                  }
 691              }
 692  
 693              /*
 694               * To be more efficient, resample large images to 5x the destination size before resizing
 695               * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
 696               * unless we would be resampling to a scale smaller than 128x128.
 697               */
 698              if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {
 699                  $resize_ratio  = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );
 700                  $sample_factor = 5;
 701  
 702                  if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
 703                      $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );
 704                  }
 705              }
 706  
 707              /*
 708               * Use resizeImage() when it's available and a valid filter value is set.
 709               * Otherwise, fall back to the scaleImage() method for resizing, which
 710               * results in better image quality over resizeImage() with default filter
 711               * settings and retains backward compatibility with pre 4.5 functionality.
 712               */
 713              if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {
 714                  $this->image->setOption( 'filter:support', '2.0' );
 715                  $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );
 716              } else {
 717                  $this->image->scaleImage( $dst_w, $dst_h );
 718              }
 719  
 720              // Set appropriate quality settings after resizing.
 721              if ( 'image/jpeg' === $this->mime_type ) {
 722                  if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
 723                      $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
 724                  }
 725  
 726                  $this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
 727              }
 728  
 729              if ( $is_png ) {
 730                  $this->image->setOption( 'png:compression-filter', '5' );
 731                  $this->image->setOption( 'png:compression-level', '9' );
 732                  $this->image->setOption( 'png:compression-strategy', '1' );
 733  
 734                  // Indexed PNG files get some additional handling.
 735                  // See #63448 for details.
 736                  if ( $is_indexed_png ) {
 737  
 738                      // Check for an alpha channel.
 739                      if ( $is_indexed_png_with_alpha_channel ) {
 740                          $this->image->setOption( 'png:include-chunk', 'tRNS' );
 741                      } else {
 742                          $this->image->setOption( 'png:exclude-chunk', 'all' );
 743                      }
 744  
 745                      $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false );
 746  
 747                      /*
 748                       * If the colorspace is 'gray', use the png8 format to ensure it stays indexed.
 749                       * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs,
 750                       * even though grayscale PNGs usually have considerably larger file sizes.
 751                       * But we can force ImageMagick to save the image as an indexed PNG instead,
 752                       * by telling it to use png8 format.
 753                       *
 754                       * Note that we need to first call quantizeImage() before checking getImageColorspace(),
 755                       * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images
 756                       * (and we have not found any other way to identify grayscale images).
 757                       *
 758                       * We need to avoid forcing indexed format for images with true alpha transparency,
 759                       * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG.
 760                       */
 761                      if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) {
 762                          // Set the image format to Indexed PNG.
 763                          $this->image->setOption( 'png:format', 'png8' );
 764                      }
 765                  } else {
 766                      $this->image->setOption( 'png:exclude-chunk', 'all' );
 767                  }
 768              }
 769  
 770              /*
 771               * If alpha channel is not defined, set it opaque.
 772               *
 773               * Note that Imagick::getImageAlphaChannel() is only available if Imagick
 774               * has been compiled against ImageMagick version 6.4.0 or newer.
 775               */
 776              if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )
 777                  && is_callable( array( $this->image, 'setImageAlphaChannel' ) )
 778                  && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
 779                  && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
 780              ) {
 781                  if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
 782                      $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
 783                  }
 784              }
 785  
 786              // Limit the bit depth of resized images.
 787              if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
 788                  /**
 789                   * Filters the maximum bit depth of resized images.
 790                   *
 791                   * This filter only applies when resizing using the Imagick editor since GD
 792                   * does not support getting or setting bit depth.
 793                   *
 794                   * Use this to adjust the maximum bit depth of resized images.
 795                   *
 796                   * @since 6.8.0
 797                   *
 798                   * @param int $max_depth   The maximum bit depth. Default is the input depth.
 799                   * @param int $image_depth The bit depth of the original image.
 800                   */
 801                  $max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() );
 802                  $this->image->setImageDepth( $max_depth );
 803              }
 804          } catch ( Exception $e ) {
 805              return new WP_Error( 'image_resize_error', $e->getMessage() );
 806          }
 807      }
 808  
 809      /**
 810       * Create multiple smaller images from a single source.
 811       *
 812       * Attempts to create all sub-sizes and returns the meta data at the end. This
 813       * may result in the server running out of resources. When it fails there may be few
 814       * "orphaned" images left over as the meta data is never returned and saved.
 815       *
 816       * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
 817       * the new images one at a time and allows for the meta data to be saved after
 818       * each new image is created.
 819       *
 820       * @since 3.5.0
 821       *
 822       * @param array $sizes {
 823       *     An array of image size data arrays.
 824       *
 825       *     Either a height or width must be provided.
 826       *     If one of the two is set to null, the resize will
 827       *     maintain aspect ratio according to the provided dimension.
 828       *
 829       *     @type array ...$0 {
 830       *         Array of height, width values, and whether to crop.
 831       *
 832       *         @type int        $width  Image width. Optional if `$height` is specified.
 833       *         @type int        $height Image height. Optional if `$width` is specified.
 834       *         @type bool|array $crop   Optional. Whether to crop the image. Default false.
 835       *     }
 836       * }
 837       * @return array An array of resized images' metadata by size.
 838       */
 839  	public function multi_resize( $sizes ) {
 840          $metadata = array();
 841  
 842          foreach ( $sizes as $size => $size_data ) {
 843              $meta = $this->make_subsize( $size_data );
 844  
 845              if ( ! is_wp_error( $meta ) ) {
 846                  $metadata[ $size ] = $meta;
 847              }
 848          }
 849  
 850          return $metadata;
 851      }
 852  
 853      /**
 854       * Create an image sub-size and return the image meta data value for it.
 855       *
 856       * @since 5.3.0
 857       *
 858       * @param array $size_data {
 859       *     Array of size data.
 860       *
 861       *     @type int        $width  The maximum width in pixels.
 862       *     @type int        $height The maximum height in pixels.
 863       *     @type bool|array $crop   Whether to crop the image to exact dimensions.
 864       * }
 865       * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
 866       *                        WP_Error object on error.
 867       */
 868  	public function make_subsize( $size_data ) {
 869          if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
 870              return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
 871          }
 872  
 873          $orig_size  = $this->size;
 874          $orig_image = $this->image->getImage();
 875  
 876          if ( ! isset( $size_data['width'] ) ) {
 877              $size_data['width'] = null;
 878          }
 879  
 880          if ( ! isset( $size_data['height'] ) ) {
 881              $size_data['height'] = null;
 882          }
 883  
 884          if ( ! isset( $size_data['crop'] ) ) {
 885              $size_data['crop'] = false;
 886          }
 887  
 888          if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) {
 889              return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) );
 890          }
 891  
 892          $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
 893  
 894          if ( is_wp_error( $resized ) ) {
 895              $saved = $resized;
 896          } else {
 897              $saved = $this->_save( $this->image );
 898  
 899              $this->image->clear();
 900              $this->image->destroy();
 901              $this->image = null;
 902          }
 903  
 904          $this->size  = $orig_size;
 905          $this->image = $orig_image;
 906  
 907          if ( ! is_wp_error( $saved ) ) {
 908              unset( $saved['path'] );
 909          }
 910  
 911          return $saved;
 912      }
 913  
 914      /**
 915       * Crops Image.
 916       *
 917       * @since 3.5.0
 918       *
 919       * @param int  $src_x   The start x position to crop from.
 920       * @param int  $src_y   The start y position to crop from.
 921       * @param int  $src_w   The width to crop.
 922       * @param int  $src_h   The height to crop.
 923       * @param int  $dst_w   Optional. The destination width.
 924       * @param int  $dst_h   Optional. The destination height.
 925       * @param bool $src_abs Optional. If the source crop points are absolute.
 926       * @return true|WP_Error
 927       */
 928  	public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
 929          if ( $src_abs ) {
 930              $src_w -= $src_x;
 931              $src_h -= $src_y;
 932          }
 933  
 934          try {
 935              $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
 936              $this->image->setImagePage( $src_w, $src_h, 0, 0 );
 937  
 938              if ( $dst_w || $dst_h ) {
 939                  /*
 940                   * If destination width/height isn't specified,
 941                   * use same as width/height from source.
 942                   */
 943                  if ( ! $dst_w ) {
 944                      $dst_w = $src_w;
 945                  }
 946                  if ( ! $dst_h ) {
 947                      $dst_h = $src_h;
 948                  }
 949  
 950                  $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
 951                  if ( is_wp_error( $thumb_result ) ) {
 952                      return $thumb_result;
 953                  }
 954  
 955                  return $this->update_size();
 956              }
 957          } catch ( Exception $e ) {
 958              return new WP_Error( 'image_crop_error', $e->getMessage() );
 959          }
 960  
 961          return $this->update_size();
 962      }
 963  
 964      /**
 965       * Rotates current image counter-clockwise by $angle.
 966       *
 967       * @since 3.5.0
 968       *
 969       * @param float $angle
 970       * @return true|WP_Error
 971       */
 972  	public function rotate( $angle ) {
 973          /**
 974           * $angle is 360-$angle because Imagick rotates clockwise
 975           * (GD rotates counter-clockwise)
 976           */
 977          try {
 978              $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
 979  
 980              // Normalize EXIF orientation data so that display is consistent across devices.
 981              if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
 982                  $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
 983              }
 984  
 985              // Since this changes the dimensions of the image, update the size.
 986              $result = $this->update_size();
 987              if ( is_wp_error( $result ) ) {
 988                  return $result;
 989              }
 990  
 991              $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
 992          } catch ( Exception $e ) {
 993              return new WP_Error( 'image_rotate_error', $e->getMessage() );
 994          }
 995  
 996          return true;
 997      }
 998  
 999      /**
1000       * Flips current image.
1001       *
1002       * @since 3.5.0
1003       *
1004       * @param bool $horz Flip along Horizontal Axis
1005       * @param bool $vert Flip along Vertical Axis
1006       * @return true|WP_Error
1007       */
1008  	public function flip( $horz, $vert ) {
1009          try {
1010              if ( $horz ) {
1011                  $this->image->flipImage();
1012              }
1013  
1014              if ( $vert ) {
1015                  $this->image->flopImage();
1016              }
1017  
1018              // Normalize EXIF orientation data so that display is consistent across devices.
1019              if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
1020                  $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
1021              }
1022          } catch ( Exception $e ) {
1023              return new WP_Error( 'image_flip_error', $e->getMessage() );
1024          }
1025  
1026          return true;
1027      }
1028  
1029      /**
1030       * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
1031       *
1032       * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
1033       * if EXIF Orientation can be reset afterwards.
1034       *
1035       * @since 5.3.0
1036       *
1037       * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
1038       *                       WP_Error if error while rotating.
1039       */
1040  	public function maybe_exif_rotate() {
1041          if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
1042              return parent::maybe_exif_rotate();
1043          } else {
1044              return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
1045          }
1046      }
1047  
1048      /**
1049       * Saves current image to file.
1050       *
1051       * @since 3.5.0
1052       * @since 6.0.0 The `$filesize` value was added to the returned array.
1053       *
1054       * @param string $destfilename Optional. Destination filename. Default null.
1055       * @param string $mime_type    Optional. The mime-type. Default null.
1056       * @return array|WP_Error {
1057       *     Array on success or WP_Error if the file failed to save.
1058       *
1059       *     @type string $path      Path to the image file.
1060       *     @type string $file      Name of the image file.
1061       *     @type int    $width     Image width.
1062       *     @type int    $height    Image height.
1063       *     @type string $mime-type The mime type of the image.
1064       *     @type int    $filesize  File size of the image.
1065       * }
1066       */
1067  	public function save( $destfilename = null, $mime_type = null ) {
1068          $saved = $this->_save( $this->image, $destfilename, $mime_type );
1069  
1070          if ( ! is_wp_error( $saved ) ) {
1071              $this->file      = $saved['path'];
1072              $this->mime_type = $saved['mime-type'];
1073  
1074              try {
1075                  $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
1076              } catch ( Exception $e ) {
1077                  return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
1078              }
1079          }
1080  
1081          return $saved;
1082      }
1083  
1084      /**
1085       * Removes PDF alpha after it's been read.
1086       *
1087       * @since 6.4.0
1088       */
1089  	protected function remove_pdf_alpha_channel() {
1090          $version = Imagick::getVersion();
1091          // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5.
1092          if ( $version['versionNumber'] >= 0x675 ) {
1093              try {
1094                  // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2.
1095                  $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 );
1096              } catch ( Exception $e ) {
1097                  return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() );
1098              }
1099          }
1100      }
1101  
1102      /**
1103       * @since 3.5.0
1104       * @since 6.0.0 The `$filesize` value was added to the returned array.
1105       *
1106       * @param Imagick $image
1107       * @param string  $filename
1108       * @param string  $mime_type
1109       * @return array|WP_Error {
1110       *     Array on success or WP_Error if the file failed to save.
1111       *
1112       *     @type string $path      Path to the image file.
1113       *     @type string $file      Name of the image file.
1114       *     @type int    $width     Image width.
1115       *     @type int    $height    Image height.
1116       *     @type string $mime-type The mime type of the image.
1117       *     @type int    $filesize  File size of the image.
1118       * }
1119       */
1120  	protected function _save( $image, $filename = null, $mime_type = null ) {
1121          list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
1122  
1123          if ( ! $filename ) {
1124              $filename = $this->generate_filename( null, null, $extension );
1125          }
1126  
1127          try {
1128              // Store initial format.
1129              $orig_format = $this->image->getImageFormat();
1130  
1131              $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
1132          } catch ( Exception $e ) {
1133              return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1134          }
1135  
1136          if ( method_exists( $this->image, 'setInterlaceScheme' )
1137              && method_exists( $this->image, 'getInterlaceScheme' )
1138              && defined( 'Imagick::INTERLACE_PLANE' )
1139          ) {
1140              $orig_interlace = $this->image->getInterlaceScheme();
1141  
1142              /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1143              if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) {
1144                  $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output.
1145              } else {
1146                  $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output.
1147              }
1148          }
1149  
1150          $write_image_result = $this->write_image( $this->image, $filename );
1151          if ( is_wp_error( $write_image_result ) ) {
1152              return $write_image_result;
1153          }
1154  
1155          try {
1156              // Reset original format.
1157              $this->image->setImageFormat( $orig_format );
1158  
1159              if ( isset( $orig_interlace ) ) {
1160                  $this->image->setInterlaceScheme( $orig_interlace );
1161              }
1162          } catch ( Exception $e ) {
1163              return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1164          }
1165  
1166          // Set correct file permissions.
1167          $stat  = stat( dirname( $filename ) );
1168          $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits.
1169          chmod( $filename, $perms );
1170  
1171          return array(
1172              'path'      => $filename,
1173              /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1174              'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
1175              'width'     => $this->size['width'],
1176              'height'    => $this->size['height'],
1177              'mime-type' => $mime_type,
1178              'filesize'  => wp_filesize( $filename ),
1179          );
1180      }
1181  
1182      /**
1183       * Writes an image to a file or stream.
1184       *
1185       * @since 5.6.0
1186       *
1187       * @param Imagick $image
1188       * @param string  $filename The destination filename or stream URL.
1189       * @return true|WP_Error
1190       */
1191  	private function write_image( $image, $filename ) {
1192          if ( wp_is_stream( $filename ) ) {
1193              /*
1194               * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead.
1195               * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php
1196               */
1197              if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) {
1198                  return new WP_Error(
1199                      'image_save_error',
1200                      sprintf(
1201                          /* translators: %s: PHP function name. */
1202                          __( '%s failed while writing image to stream.' ),
1203                          '<code>file_put_contents()</code>'
1204                      ),
1205                      $filename
1206                  );
1207              } else {
1208                  return true;
1209              }
1210          } else {
1211              $dirname = dirname( $filename );
1212  
1213              if ( ! wp_mkdir_p( $dirname ) ) {
1214                  return new WP_Error(
1215                      'image_save_error',
1216                      sprintf(
1217                          /* translators: %s: Directory path. */
1218                          __( 'Unable to create directory %s. Is its parent directory writable by the server?' ),
1219                          esc_html( $dirname )
1220                      )
1221                  );
1222              }
1223  
1224              try {
1225                  return $image->writeImage( $filename );
1226              } catch ( Exception $e ) {
1227                  return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1228              }
1229          }
1230      }
1231  
1232      /**
1233       * Streams current image to browser.
1234       *
1235       * @since 3.5.0
1236       *
1237       * @param string $mime_type The mime type of the image.
1238       * @return true|WP_Error True on success, WP_Error object on failure.
1239       */
1240  	public function stream( $mime_type = null ) {
1241          list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
1242  
1243          try {
1244              // Temporarily change format for stream.
1245              $this->image->setImageFormat( strtoupper( $extension ) );
1246  
1247              // Output stream of image content.
1248              header( "Content-Type: $mime_type" );
1249              print $this->image->getImageBlob();
1250  
1251              // Reset image to original format.
1252              $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
1253          } catch ( Exception $e ) {
1254              return new WP_Error( 'image_stream_error', $e->getMessage() );
1255          }
1256  
1257          return true;
1258      }
1259  
1260      /**
1261       * Strips all image meta except color profiles from an image.
1262       *
1263       * @since 4.5.0
1264       *
1265       * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
1266       */
1267  	protected function strip_meta() {
1268  
1269          if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
1270              return new WP_Error(
1271                  'image_strip_meta_error',
1272                  sprintf(
1273                      /* translators: %s: ImageMagick method name. */
1274                      __( '%s is required to strip image meta.' ),
1275                      '<code>Imagick::getImageProfiles()</code>'
1276                  )
1277              );
1278          }
1279  
1280          if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
1281              return new WP_Error(
1282                  'image_strip_meta_error',
1283                  sprintf(
1284                      /* translators: %s: ImageMagick method name. */
1285                      __( '%s is required to strip image meta.' ),
1286                      '<code>Imagick::removeImageProfile()</code>'
1287                  )
1288              );
1289          }
1290  
1291          /*
1292           * Protect a few profiles from being stripped for the following reasons:
1293           *
1294           * - icc:  Color profile information
1295           * - icm:  Color profile information
1296           * - iptc: Copyright data
1297           * - exif: Orientation data
1298           * - xmp:  Rights usage data
1299           */
1300          $protected_profiles = array(
1301              'icc',
1302              'icm',
1303              'iptc',
1304              'exif',
1305              'xmp',
1306          );
1307  
1308          try {
1309              // Strip profiles.
1310              foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
1311                  if ( ! in_array( $key, $protected_profiles, true ) ) {
1312                      $this->image->removeImageProfile( $key );
1313                  }
1314              }
1315          } catch ( Exception $e ) {
1316              return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
1317          }
1318  
1319          return true;
1320      }
1321  
1322      /**
1323       * Sets up Imagick for PDF processing.
1324       * Increases rendering DPI and only loads first page.
1325       *
1326       * @since 4.7.0
1327       *
1328       * @return string|WP_Error File to load or WP_Error on failure.
1329       */
1330  	protected function pdf_setup() {
1331          try {
1332              /*
1333               * By default, PDFs are rendered in a very low resolution.
1334               * We want the thumbnail to be readable, so increase the rendering DPI.
1335               */
1336              $this->image->setResolution( 128, 128 );
1337  
1338              // Only load the first page.
1339              return $this->file . '[0]';
1340          } catch ( Exception $e ) {
1341              return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
1342          }
1343      }
1344  
1345      /**
1346       * Load the image produced by Ghostscript.
1347       *
1348       * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files
1349       * when `use-cropbox` is set.
1350       *
1351       * @since 5.6.0
1352       *
1353       * @return true|WP_Error
1354       */
1355  	protected function pdf_load_source() {
1356          $filename = $this->pdf_setup();
1357  
1358          if ( is_wp_error( $filename ) ) {
1359              return $filename;
1360          }
1361  
1362          foreach ( array( 'true', 'false' ) as $use_cropbox ) {
1363              try {
1364                  /**
1365                   * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
1366                   * area (resulting in unnecessary whitespace) unless the following option is set.
1367                   *
1368                   * However, it sometimes fails, so if that happens, run it without the option.
1369                   *
1370                   * @ticket 48853
1371                   */
1372                  $this->image->setOption( 'pdf:use-cropbox', $use_cropbox );
1373  
1374                  /*
1375                   * Reading image after Imagick instantiation because `setResolution`
1376                   * only applies correctly before the image is read.
1377                   */
1378                  if ( is_string( $this->stream_file_data ) ) {
1379                      $this->image->setFilename( 'PDF:unknown.pdf[0]' );
1380                      $this->image->readImageBlob( $this->stream_file_data, $this->image_given_name );
1381                  } else {
1382                      $this->image->readImage( $filename );
1383                  }
1384  
1385                  return true;
1386              } catch ( Exception $e ) {
1387                  continue;
1388              }
1389          }
1390  
1391          return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
1392      }
1393  }


Generated : Wed Aug 26 08:20:24 2026 Cross-referenced by PHPXref