[ 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          return null;
 536      }
 537  
 538      /**
 539       * Resizes current image.
 540       *
 541       * At minimum, either a height or width must be provided.
 542       * If one of the two is set to null, the resize will
 543       * maintain aspect ratio according to the provided dimension.
 544       *
 545       * @since 3.5.0
 546       *
 547       * @param int|null   $max_w Image width.
 548       * @param int|null   $max_h Image height.
 549       * @param bool|array $crop  {
 550       *     Optional. Image cropping behavior. If false, the image will be scaled (default).
 551       *     If true, image will be cropped to the specified dimensions using center positions.
 552       *     If an array, the image will be cropped using the array to specify the crop location:
 553       *
 554       *     @type string $0 The x crop position. Accepts 'left', 'center', or 'right'.
 555       *     @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
 556       * }
 557       * @return true|WP_Error
 558       */
 559  	public function resize( $max_w, $max_h, $crop = false ) {
 560          if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) {
 561              return true;
 562          }
 563  
 564          $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
 565          if ( ! $dims ) {
 566              return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
 567          }
 568  
 569          list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
 570  
 571          if ( $crop ) {
 572              return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
 573          }
 574  
 575          $this->set_quality(
 576              null,
 577              array(
 578                  'width'  => $dst_w,
 579                  'height' => $dst_h,
 580              )
 581          );
 582  
 583          // Execute the resize.
 584          $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
 585          if ( is_wp_error( $thumb_result ) ) {
 586              return $thumb_result;
 587          }
 588  
 589          return $this->update_size( $dst_w, $dst_h );
 590      }
 591  
 592      /**
 593       * Efficiently resize the current image
 594       *
 595       * This is a WordPress specific implementation of Imagick::thumbnailImage(),
 596       * which resizes an image to given dimensions and removes any associated profiles.
 597       *
 598       * @since 4.5.0
 599       *
 600       * @param int    $dst_w       The destination width.
 601       * @param int    $dst_h       The destination height.
 602       * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
 603       * @param bool   $strip_meta  Optional. Strip all profiles, excluding color profiles, from the image. Default true.
 604       * @return void|WP_Error
 605       */
 606  	protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
 607          $allowed_filters = array(
 608              'FILTER_POINT',
 609              'FILTER_BOX',
 610              'FILTER_TRIANGLE',
 611              'FILTER_HERMITE',
 612              'FILTER_HANNING',
 613              'FILTER_HAMMING',
 614              'FILTER_BLACKMAN',
 615              'FILTER_GAUSSIAN',
 616              'FILTER_QUADRATIC',
 617              'FILTER_CUBIC',
 618              'FILTER_CATROM',
 619              'FILTER_MITCHELL',
 620              'FILTER_LANCZOS',
 621              'FILTER_BESSEL',
 622              'FILTER_SINC',
 623          );
 624  
 625          /**
 626           * Set the filter value if '$filter_name' name is in the allowed list and the related
 627           * Imagick constant is defined or fall back to the default filter.
 628           */
 629          if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
 630              $filter = constant( 'Imagick::' . $filter_name );
 631          } else {
 632              $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
 633          }
 634  
 635          /**
 636           * Filters whether to strip metadata from images when they're resized.
 637           *
 638           * This filter only applies when resizing using the Imagick editor since GD
 639           * always strips profiles by default.
 640           *
 641           * @since 4.5.0
 642           *
 643           * @param bool $strip_meta Whether to strip image metadata during resizing. Default true.
 644           */
 645          if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {
 646              $this->strip_meta(); // Fail silently if not supported.
 647          }
 648  
 649          try {
 650              /**
 651               * Special handling for certain types of PNG images:
 652               * 1. For PNG images, we need to specify compression settings and remove unneeded chunks.
 653               * 2. For indexed PNG images, the number of colors must not exceed 256.
 654               * 3. For indexed PNG images with an alpha channel, the tRNS chunk must be preserved.
 655               * 4. For indexed PNG images with true alpha transparency (an alpha channel > 1 bit), we need to avoid saving
 656               * the image using ImageMagick's 'png8' format,  because that supports only binary (1 bit) transparency.
 657               *
 658               * For #4 we want to check whether the image has a 1-bit alpha channel before resizing,  because resizing
 659               * may cause the number of alpha values to multiply due to antialiasing. If the original image had only a
 660               * 1-bit alpha channel, then a 1-bit alpha channel should be good enough for the resized images.
 661               *
 662               * Perform all the necessary checks before resizing the image and store the results in variables for later use.
 663               */
 664              $is_png                                      = false;
 665              $is_indexed_png                              = false;
 666              $is_indexed_png_with_alpha_channel           = false;
 667              $is_indexed_png_with_true_alpha_transparency = false;
 668  
 669              if ( 'image/png' === $this->mime_type ) {
 670                  $is_png = true;
 671  
 672                  if (
 673                      is_callable( array( $this->image, 'getImageProperty' ) )
 674                      && '3' === $this->image->getImageProperty( 'png:IHDR.color-type-orig' )
 675                  ) {
 676                      $is_indexed_png = true;
 677  
 678                      if (
 679                          is_callable( array( $this->image, 'getImageAlphaChannel' ) )
 680                          && $this->image->getImageAlphaChannel()
 681                      ) {
 682                          $is_indexed_png_with_alpha_channel = true;
 683  
 684                          if (
 685                              is_callable( array( $this->image, 'getImageChannelDepth' ) )
 686                              && defined( 'Imagick::CHANNEL_ALPHA' )
 687                              && 1 < $this->image->getImageChannelDepth( Imagick::CHANNEL_ALPHA )
 688                          ) {
 689                              $is_indexed_png_with_true_alpha_transparency = true;
 690                          }
 691                      }
 692                  }
 693              }
 694  
 695              /*
 696               * To be more efficient, resample large images to 5x the destination size before resizing
 697               * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
 698               * unless we would be resampling to a scale smaller than 128x128.
 699               */
 700              if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {
 701                  $resize_ratio  = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );
 702                  $sample_factor = 5;
 703  
 704                  if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
 705                      $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );
 706                  }
 707              }
 708  
 709              /*
 710               * Use resizeImage() when it's available and a valid filter value is set.
 711               * Otherwise, fall back to the scaleImage() method for resizing, which
 712               * results in better image quality over resizeImage() with default filter
 713               * settings and retains backward compatibility with pre 4.5 functionality.
 714               */
 715              if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {
 716                  $this->image->setOption( 'filter:support', '2.0' );
 717                  $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );
 718              } else {
 719                  $this->image->scaleImage( $dst_w, $dst_h );
 720              }
 721  
 722              // Set appropriate quality settings after resizing.
 723              if ( 'image/jpeg' === $this->mime_type ) {
 724                  if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
 725                      $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
 726                  }
 727  
 728                  $this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
 729              }
 730  
 731              if ( $is_png ) {
 732                  $this->image->setOption( 'png:compression-filter', '5' );
 733                  $this->image->setOption( 'png:compression-level', '9' );
 734                  $this->image->setOption( 'png:compression-strategy', '1' );
 735  
 736                  // Indexed PNG files get some additional handling.
 737                  // See #63448 for details.
 738                  if ( $is_indexed_png ) {
 739  
 740                      // Check for an alpha channel.
 741                      if ( $is_indexed_png_with_alpha_channel ) {
 742                          $this->image->setOption( 'png:include-chunk', 'tRNS' );
 743                      } else {
 744                          $this->image->setOption( 'png:exclude-chunk', 'all' );
 745                      }
 746  
 747                      $this->image->quantizeImage( 256, $this->image->getColorspace(), 0, false, false );
 748  
 749                      /*
 750                       * If the colorspace is 'gray', use the png8 format to ensure it stays indexed.
 751                       * ImageMagick tends to save grayscale images as grayscale PNGs rather than indexed PNGs,
 752                       * even though grayscale PNGs usually have considerably larger file sizes.
 753                       * But we can force ImageMagick to save the image as an indexed PNG instead,
 754                       * by telling it to use png8 format.
 755                       *
 756                       * Note that we need to first call quantizeImage() before checking getImageColorspace(),
 757                       * because only after calling quantizeImage() will the colorspace be COLORSPACE_GRAY for grayscale images
 758                       * (and we have not found any other way to identify grayscale images).
 759                       *
 760                       * We need to avoid forcing indexed format for images with true alpha transparency,
 761                       * because ImageMagick does not support saving an image with true alpha transparency as an indexed PNG.
 762                       */
 763                      if ( Imagick::COLORSPACE_GRAY === $this->image->getImageColorspace() && ! $is_indexed_png_with_true_alpha_transparency ) {
 764                          // Set the image format to Indexed PNG.
 765                          $this->image->setOption( 'png:format', 'png8' );
 766                      }
 767                  } else {
 768                      $this->image->setOption( 'png:exclude-chunk', 'all' );
 769                  }
 770              }
 771  
 772              /*
 773               * If alpha channel is not defined, set it opaque.
 774               *
 775               * Note that Imagick::getImageAlphaChannel() is only available if Imagick
 776               * has been compiled against ImageMagick version 6.4.0 or newer.
 777               */
 778              if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )
 779                  && is_callable( array( $this->image, 'setImageAlphaChannel' ) )
 780                  && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
 781                  && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
 782              ) {
 783                  if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
 784                      $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
 785                  }
 786              }
 787  
 788              // Limit the bit depth of resized images.
 789              if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
 790                  /**
 791                   * Filters the maximum bit depth of resized images.
 792                   *
 793                   * This filter only applies when resizing using the Imagick editor since GD
 794                   * does not support getting or setting bit depth.
 795                   *
 796                   * Use this to adjust the maximum bit depth of resized images.
 797                   *
 798                   * @since 6.8.0
 799                   *
 800                   * @param int $max_depth   The maximum bit depth. Default is the input depth.
 801                   * @param int $image_depth The bit depth of the original image.
 802                   */
 803                  $max_depth = apply_filters( 'image_max_bit_depth', $this->image->getImageDepth(), $this->image->getImageDepth() );
 804                  $this->image->setImageDepth( $max_depth );
 805              }
 806          } catch ( Exception $e ) {
 807              return new WP_Error( 'image_resize_error', $e->getMessage() );
 808          }
 809      }
 810  
 811      /**
 812       * Create multiple smaller images from a single source.
 813       *
 814       * Attempts to create all sub-sizes and returns the meta data at the end. This
 815       * may result in the server running out of resources. When it fails there may be few
 816       * "orphaned" images left over as the meta data is never returned and saved.
 817       *
 818       * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
 819       * the new images one at a time and allows for the meta data to be saved after
 820       * each new image is created.
 821       *
 822       * @since 3.5.0
 823       *
 824       * @param array $sizes {
 825       *     An array of image size data arrays.
 826       *
 827       *     Either a height or width must be provided.
 828       *     If one of the two is set to null, the resize will
 829       *     maintain aspect ratio according to the provided dimension.
 830       *
 831       *     @type array ...$0 {
 832       *         Array of height, width values, and whether to crop.
 833       *
 834       *         @type int        $width  Image width. Optional if `$height` is specified.
 835       *         @type int        $height Image height. Optional if `$width` is specified.
 836       *         @type bool|array $crop   Optional. Whether to crop the image. Default false.
 837       *     }
 838       * }
 839       * @return array An array of resized images' metadata by size.
 840       */
 841  	public function multi_resize( $sizes ) {
 842          $metadata = array();
 843  
 844          foreach ( $sizes as $size => $size_data ) {
 845              $meta = $this->make_subsize( $size_data );
 846  
 847              if ( ! is_wp_error( $meta ) ) {
 848                  $metadata[ $size ] = $meta;
 849              }
 850          }
 851  
 852          return $metadata;
 853      }
 854  
 855      /**
 856       * Create an image sub-size and return the image meta data value for it.
 857       *
 858       * @since 5.3.0
 859       *
 860       * @param array $size_data {
 861       *     Array of size data.
 862       *
 863       *     @type int        $width  The maximum width in pixels.
 864       *     @type int        $height The maximum height in pixels.
 865       *     @type bool|array $crop   Whether to crop the image to exact dimensions.
 866       * }
 867       * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
 868       *                        WP_Error object on error.
 869       */
 870  	public function make_subsize( $size_data ) {
 871          if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
 872              return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
 873          }
 874  
 875          $orig_size  = $this->size;
 876          $orig_image = $this->image->getImage();
 877  
 878          $size_data['width']  ??= null;
 879          $size_data['height'] ??= null;
 880          $size_data['crop']   ??= false;
 881  
 882          if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) {
 883              return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) );
 884          }
 885  
 886          $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
 887  
 888          if ( is_wp_error( $resized ) ) {
 889              $saved = $resized;
 890          } else {
 891              $saved = $this->_save( $this->image );
 892  
 893              $this->image->clear();
 894              $this->image->destroy();
 895              $this->image = null;
 896          }
 897  
 898          $this->size  = $orig_size;
 899          $this->image = $orig_image;
 900  
 901          if ( ! is_wp_error( $saved ) ) {
 902              unset( $saved['path'] );
 903          }
 904  
 905          return $saved;
 906      }
 907  
 908      /**
 909       * Crops Image.
 910       *
 911       * @since 3.5.0
 912       *
 913       * @param int  $src_x   The start x position to crop from.
 914       * @param int  $src_y   The start y position to crop from.
 915       * @param int  $src_w   The width to crop.
 916       * @param int  $src_h   The height to crop.
 917       * @param int  $dst_w   Optional. The destination width.
 918       * @param int  $dst_h   Optional. The destination height.
 919       * @param bool $src_abs Optional. If the source crop points are absolute.
 920       * @return true|WP_Error
 921       */
 922  	public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
 923          if ( $src_abs ) {
 924              $src_w -= $src_x;
 925              $src_h -= $src_y;
 926          }
 927  
 928          try {
 929              $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
 930              $this->image->setImagePage( $src_w, $src_h, 0, 0 );
 931  
 932              if ( $dst_w || $dst_h ) {
 933                  /*
 934                   * If destination width/height isn't specified,
 935                   * use same as width/height from source.
 936                   */
 937                  if ( ! $dst_w ) {
 938                      $dst_w = $src_w;
 939                  }
 940                  if ( ! $dst_h ) {
 941                      $dst_h = $src_h;
 942                  }
 943  
 944                  $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
 945                  if ( is_wp_error( $thumb_result ) ) {
 946                      return $thumb_result;
 947                  }
 948  
 949                  return $this->update_size();
 950              }
 951          } catch ( Exception $e ) {
 952              return new WP_Error( 'image_crop_error', $e->getMessage() );
 953          }
 954  
 955          return $this->update_size();
 956      }
 957  
 958      /**
 959       * Rotates current image counter-clockwise by $angle.
 960       *
 961       * @since 3.5.0
 962       *
 963       * @param float $angle
 964       * @return true|WP_Error
 965       */
 966  	public function rotate( $angle ) {
 967          /**
 968           * $angle is 360-$angle because Imagick rotates clockwise
 969           * (GD rotates counter-clockwise)
 970           */
 971          try {
 972              $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
 973  
 974              // Normalize EXIF orientation data so that display is consistent across devices.
 975              if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
 976                  $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
 977              }
 978  
 979              // Since this changes the dimensions of the image, update the size.
 980              $result = $this->update_size();
 981              if ( is_wp_error( $result ) ) {
 982                  return $result;
 983              }
 984  
 985              $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
 986          } catch ( Exception $e ) {
 987              return new WP_Error( 'image_rotate_error', $e->getMessage() );
 988          }
 989  
 990          return true;
 991      }
 992  
 993      /**
 994       * Flips current image.
 995       *
 996       * @since 3.5.0
 997       *
 998       * @param bool $horz Flip along Horizontal Axis
 999       * @param bool $vert Flip along Vertical Axis
1000       * @return true|WP_Error
1001       */
1002  	public function flip( $horz, $vert ) {
1003          try {
1004              if ( $horz ) {
1005                  $this->image->flipImage();
1006              }
1007  
1008              if ( $vert ) {
1009                  $this->image->flopImage();
1010              }
1011  
1012              // Normalize EXIF orientation data so that display is consistent across devices.
1013              if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
1014                  $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
1015              }
1016          } catch ( Exception $e ) {
1017              return new WP_Error( 'image_flip_error', $e->getMessage() );
1018          }
1019  
1020          return true;
1021      }
1022  
1023      /**
1024       * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
1025       *
1026       * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
1027       * if EXIF Orientation can be reset afterwards.
1028       *
1029       * @since 5.3.0
1030       *
1031       * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
1032       *                       WP_Error if error while rotating.
1033       */
1034  	public function maybe_exif_rotate() {
1035          if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
1036              return parent::maybe_exif_rotate();
1037          } else {
1038              return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
1039          }
1040      }
1041  
1042      /**
1043       * Saves current image to file.
1044       *
1045       * @since 3.5.0
1046       * @since 6.0.0 The `$filesize` value was added to the returned array.
1047       *
1048       * @param string $destfilename Optional. Destination filename. Default null.
1049       * @param string $mime_type    Optional. The mime-type. Default null.
1050       * @return array|WP_Error {
1051       *     Array on success or WP_Error if the file failed to save.
1052       *
1053       *     @type string $path      Path to the image file.
1054       *     @type string $file      Name of the image file.
1055       *     @type int    $width     Image width.
1056       *     @type int    $height    Image height.
1057       *     @type string $mime-type The mime type of the image.
1058       *     @type int    $filesize  File size of the image.
1059       * }
1060       */
1061  	public function save( $destfilename = null, $mime_type = null ) {
1062          $saved = $this->_save( $this->image, $destfilename, $mime_type );
1063  
1064          if ( ! is_wp_error( $saved ) ) {
1065              $this->file      = $saved['path'];
1066              $this->mime_type = $saved['mime-type'];
1067  
1068              try {
1069                  $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
1070              } catch ( Exception $e ) {
1071                  return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
1072              }
1073          }
1074  
1075          return $saved;
1076      }
1077  
1078      /**
1079       * Removes PDF alpha after it's been read.
1080       *
1081       * @since 6.4.0
1082       *
1083       * @return null|WP_Error Null on success, WP_Error object if the alpha channel could not be removed.
1084       */
1085  	protected function remove_pdf_alpha_channel() {
1086          $version = Imagick::getVersion();
1087          // Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5.
1088          if ( $version['versionNumber'] >= 0x675 ) {
1089              try {
1090                  // Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2.
1091                  $this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 );
1092              } catch ( Exception $e ) {
1093                  return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() );
1094              }
1095          }
1096  
1097          return null;
1098      }
1099  
1100      /**
1101       * @since 3.5.0
1102       * @since 6.0.0 The `$filesize` value was added to the returned array.
1103       *
1104       * @param Imagick $image
1105       * @param string  $filename
1106       * @param string  $mime_type
1107       * @return array|WP_Error {
1108       *     Array on success or WP_Error if the file failed to save.
1109       *
1110       *     @type string $path      Path to the image file.
1111       *     @type string $file      Name of the image file.
1112       *     @type int    $width     Image width.
1113       *     @type int    $height    Image height.
1114       *     @type string $mime-type The mime type of the image.
1115       *     @type int    $filesize  File size of the image.
1116       * }
1117       */
1118  	protected function _save( $image, $filename = null, $mime_type = null ) {
1119          list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
1120  
1121          if ( ! $filename ) {
1122              $filename = $this->generate_filename( null, null, $extension );
1123          }
1124  
1125          try {
1126              // Store initial format.
1127              $orig_format = $this->image->getImageFormat();
1128  
1129              $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
1130          } catch ( Exception $e ) {
1131              return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1132          }
1133  
1134          if ( method_exists( $this->image, 'setInterlaceScheme' )
1135              && method_exists( $this->image, 'getInterlaceScheme' )
1136              && defined( 'Imagick::INTERLACE_PLANE' )
1137          ) {
1138              $orig_interlace = $this->image->getInterlaceScheme();
1139  
1140              /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1141              if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) {
1142                  $this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output.
1143              } else {
1144                  $this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output.
1145              }
1146          }
1147  
1148          $write_image_result = $this->write_image( $this->image, $filename );
1149          if ( is_wp_error( $write_image_result ) ) {
1150              return $write_image_result;
1151          }
1152  
1153          try {
1154              // Reset original format.
1155              $this->image->setImageFormat( $orig_format );
1156  
1157              if ( isset( $orig_interlace ) ) {
1158                  $this->image->setInterlaceScheme( $orig_interlace );
1159              }
1160          } catch ( Exception $e ) {
1161              return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1162          }
1163  
1164          // Set correct file permissions.
1165          $stat  = stat( dirname( $filename ) );
1166          $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits.
1167          chmod( $filename, $perms );
1168  
1169          return array(
1170              'path'      => $filename,
1171              /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
1172              'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
1173              'width'     => $this->size['width'],
1174              'height'    => $this->size['height'],
1175              'mime-type' => $mime_type,
1176              'filesize'  => wp_filesize( $filename ),
1177          );
1178      }
1179  
1180      /**
1181       * Writes an image to a file or stream.
1182       *
1183       * @since 5.6.0
1184       *
1185       * @param Imagick $image
1186       * @param string  $filename The destination filename or stream URL.
1187       * @return true|WP_Error
1188       */
1189  	private function write_image( $image, $filename ) {
1190          if ( wp_is_stream( $filename ) ) {
1191              /*
1192               * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead.
1193               * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php
1194               */
1195              if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) {
1196                  return new WP_Error(
1197                      'image_save_error',
1198                      sprintf(
1199                          /* translators: %s: PHP function name. */
1200                          __( '%s failed while writing image to stream.' ),
1201                          '<code>file_put_contents()</code>'
1202                      ),
1203                      $filename
1204                  );
1205              } else {
1206                  return true;
1207              }
1208          } else {
1209              $dirname = dirname( $filename );
1210  
1211              if ( ! wp_mkdir_p( $dirname ) ) {
1212                  return new WP_Error(
1213                      'image_save_error',
1214                      sprintf(
1215                          /* translators: %s: Directory path. */
1216                          __( 'Unable to create directory %s. Is its parent directory writable by the server?' ),
1217                          esc_html( $dirname )
1218                      )
1219                  );
1220              }
1221  
1222              try {
1223                  return $image->writeImage( $filename );
1224              } catch ( Exception $e ) {
1225                  return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
1226              }
1227          }
1228      }
1229  
1230      /**
1231       * Streams current image to browser.
1232       *
1233       * @since 3.5.0
1234       *
1235       * @param string $mime_type The mime type of the image.
1236       * @return true|WP_Error True on success, WP_Error object on failure.
1237       */
1238  	public function stream( $mime_type = null ) {
1239          list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
1240  
1241          try {
1242              // Temporarily change format for stream.
1243              $this->image->setImageFormat( strtoupper( $extension ) );
1244  
1245              // Output stream of image content.
1246              header( "Content-Type: $mime_type" );
1247              print $this->image->getImageBlob();
1248  
1249              // Reset image to original format.
1250              $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
1251          } catch ( Exception $e ) {
1252              return new WP_Error( 'image_stream_error', $e->getMessage() );
1253          }
1254  
1255          return true;
1256      }
1257  
1258      /**
1259       * Strips all image meta except color profiles from an image.
1260       *
1261       * @since 4.5.0
1262       *
1263       * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
1264       */
1265  	protected function strip_meta() {
1266  
1267          if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
1268              return new WP_Error(
1269                  'image_strip_meta_error',
1270                  sprintf(
1271                      /* translators: %s: ImageMagick method name. */
1272                      __( '%s is required to strip image meta.' ),
1273                      '<code>Imagick::getImageProfiles()</code>'
1274                  )
1275              );
1276          }
1277  
1278          if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
1279              return new WP_Error(
1280                  'image_strip_meta_error',
1281                  sprintf(
1282                      /* translators: %s: ImageMagick method name. */
1283                      __( '%s is required to strip image meta.' ),
1284                      '<code>Imagick::removeImageProfile()</code>'
1285                  )
1286              );
1287          }
1288  
1289          /*
1290           * Protect a few profiles from being stripped for the following reasons:
1291           *
1292           * - icc:  Color profile information
1293           * - icm:  Color profile information
1294           * - iptc: Copyright data
1295           * - exif: Orientation data
1296           * - xmp:  Rights usage data
1297           */
1298          $protected_profiles = array(
1299              'icc',
1300              'icm',
1301              'iptc',
1302              'exif',
1303              'xmp',
1304          );
1305  
1306          try {
1307              // Strip profiles.
1308              foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
1309                  if ( ! in_array( $key, $protected_profiles, true ) ) {
1310                      $this->image->removeImageProfile( $key );
1311                  }
1312              }
1313          } catch ( Exception $e ) {
1314              return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
1315          }
1316  
1317          return true;
1318      }
1319  
1320      /**
1321       * Sets up Imagick for PDF processing.
1322       * Increases rendering DPI and only loads first page.
1323       *
1324       * @since 4.7.0
1325       *
1326       * @return string|WP_Error File to load or WP_Error on failure.
1327       */
1328  	protected function pdf_setup() {
1329          try {
1330              /*
1331               * By default, PDFs are rendered in a very low resolution.
1332               * We want the thumbnail to be readable, so increase the rendering DPI.
1333               */
1334              $this->image->setResolution( 128, 128 );
1335  
1336              // Only load the first page.
1337              return $this->file . '[0]';
1338          } catch ( Exception $e ) {
1339              return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
1340          }
1341      }
1342  
1343      /**
1344       * Load the image produced by Ghostscript.
1345       *
1346       * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files
1347       * when `use-cropbox` is set.
1348       *
1349       * @since 5.6.0
1350       *
1351       * @return true|WP_Error
1352       */
1353  	protected function pdf_load_source() {
1354          $filename = $this->pdf_setup();
1355  
1356          if ( is_wp_error( $filename ) ) {
1357              return $filename;
1358          }
1359  
1360          foreach ( array( 'true', 'false' ) as $use_cropbox ) {
1361              try {
1362                  /**
1363                   * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
1364                   * area (resulting in unnecessary whitespace) unless the following option is set.
1365                   *
1366                   * However, it sometimes fails, so if that happens, run it without the option.
1367                   *
1368                   * @ticket 48853
1369                   */
1370                  $this->image->setOption( 'pdf:use-cropbox', $use_cropbox );
1371  
1372                  /*
1373                   * Reading image after Imagick instantiation because `setResolution`
1374                   * only applies correctly before the image is read.
1375                   */
1376                  if ( is_string( $this->stream_file_data ) ) {
1377                      $this->image->setFilename( 'PDF:unknown.pdf[0]' );
1378                      $this->image->readImageBlob( $this->stream_file_data, $this->image_given_name );
1379                  } else {
1380                      $this->image->readImage( $filename );
1381                  }
1382  
1383                  return true;
1384              } catch ( Exception $e ) {
1385                  continue;
1386              }
1387          }
1388  
1389          return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
1390      }
1391  }


Generated : Tue Sep 15 08:20:32 2026 Cross-referenced by PHPXref