wp/wp-includes/class-wp-image-editor-imagick.php
changeset 21 48c4eec2b7e6
parent 19 3d72ae0968f4
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
   100 
   100 
   101 		if ( ! $imagick_extension ) {
   101 		if ( ! $imagick_extension ) {
   102 			return false;
   102 			return false;
   103 		}
   103 		}
   104 
   104 
   105 		// setIteratorIndex is optional unless mime is an animated format.
   105 		/*
   106 		// Here, we just say no if you are missing it and aren't loading a jpeg.
   106 		 * setIteratorIndex is optional unless mime is an animated format.
       
   107 		 * Here, we just say no if you are missing it and aren't loading a jpeg.
       
   108 		 */
   107 		if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
   109 		if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
   108 				return false;
   110 				return false;
   109 		}
   111 		}
   110 
   112 
   111 		try {
   113 		try {
   164 			// Select the first frame to handle animated images properly.
   166 			// Select the first frame to handle animated images properly.
   165 			if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
   167 			if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
   166 				$this->image->setIteratorIndex( 0 );
   168 				$this->image->setIteratorIndex( 0 );
   167 			}
   169 			}
   168 
   170 
       
   171 			if ( 'pdf' === $file_extension ) {
       
   172 				$this->remove_pdf_alpha_channel();
       
   173 			}
       
   174 
   169 			$this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
   175 			$this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
   170 		} catch ( Exception $e ) {
   176 		} catch ( Exception $e ) {
   171 			return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
   177 			return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
   172 		}
   178 		}
   173 
   179 
   211 						$this->image->setOption( 'webp:lossless', 'true' );
   217 						$this->image->setOption( 'webp:lossless', 'true' );
   212 					} else {
   218 					} else {
   213 						$this->image->setImageCompressionQuality( $quality );
   219 						$this->image->setImageCompressionQuality( $quality );
   214 					}
   220 					}
   215 					break;
   221 					break;
       
   222 				case 'image/avif':
   216 				default:
   223 				default:
   217 					$this->image->setImageCompressionQuality( $quality );
   224 					$this->image->setImageCompressionQuality( $quality );
   218 			}
   225 			}
   219 		} catch ( Exception $e ) {
   226 		} catch ( Exception $e ) {
   220 			return new WP_Error( 'image_quality_error', $e->getMessage() );
   227 			return new WP_Error( 'image_quality_error', $e->getMessage() );
   248 
   255 
   249 		if ( ! $height ) {
   256 		if ( ! $height ) {
   250 			$height = $size['height'];
   257 			$height = $size['height'];
   251 		}
   258 		}
   252 
   259 
       
   260 		/*
       
   261 		 * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images
       
   262 		 * are properly sized without affecting previous `getImageGeometry` behavior.
       
   263 		 */
       
   264 		if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) {
       
   265 			$size   = wp_getimagesize( $this->file );
       
   266 			$width  = $size[0];
       
   267 			$height = $size[1];
       
   268 		}
       
   269 
   253 		return parent::update_size( $width, $height );
   270 		return parent::update_size( $width, $height );
       
   271 	}
       
   272 
       
   273 	/**
       
   274 	 * Sets Imagick time limit.
       
   275 	 *
       
   276 	 * Depending on configuration, Imagick processing may take time.
       
   277 	 *
       
   278 	 * Multiple problems exist if PHP times out before ImageMagick completed:
       
   279 	 * 1. Temporary files aren't cleaned by ImageMagick garbage collection.
       
   280 	 * 2. No clear error is provided.
       
   281 	 * 3. The cause of such timeout can be hard to pinpoint.
       
   282 	 *
       
   283 	 * This function, which is expected to be run before heavy image routines, resolves
       
   284 	 * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it is set.
       
   285 	 *
       
   286 	 * However seems it introduces more problems than it fixes,
       
   287 	 * see https://core.trac.wordpress.org/ticket/58202.
       
   288 	 *
       
   289 	 * Note:
       
   290 	 *  - Imagick resource exhaustion does not issue catchable exceptions (yet).
       
   291 	 *    See https://github.com/Imagick/imagick/issues/333.
       
   292 	 *  - The resource limit is not saved/restored. It applies to subsequent
       
   293 	 *    image operations within the time of the HTTP request.
       
   294 	 *
       
   295 	 * @since 6.2.0
       
   296 	 * @since 6.3.0 This method was deprecated.
       
   297 	 *
       
   298 	 * @return int|null The new limit on success, null on failure.
       
   299 	 */
       
   300 	public static function set_imagick_time_limit() {
       
   301 		_deprecated_function( __METHOD__, '6.3.0' );
       
   302 
       
   303 		if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) {
       
   304 			return null;
       
   305 		}
       
   306 
       
   307 		// Returns PHP_FLOAT_MAX if unset.
       
   308 		$imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME );
       
   309 
       
   310 		// Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX.
       
   311 		$imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout;
       
   312 
       
   313 		$php_timeout = (int) ini_get( 'max_execution_time' );
       
   314 
       
   315 		if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) {
       
   316 			$limit = (float) 0.8 * $php_timeout;
       
   317 			Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit );
       
   318 
       
   319 			return $limit;
       
   320 		}
   254 	}
   321 	}
   255 
   322 
   256 	/**
   323 	/**
   257 	 * Resizes current image.
   324 	 * Resizes current image.
   258 	 *
   325 	 *
   260 	 * If one of the two is set to null, the resize will
   327 	 * If one of the two is set to null, the resize will
   261 	 * maintain aspect ratio according to the provided dimension.
   328 	 * maintain aspect ratio according to the provided dimension.
   262 	 *
   329 	 *
   263 	 * @since 3.5.0
   330 	 * @since 3.5.0
   264 	 *
   331 	 *
   265 	 * @param int|null $max_w Image width.
   332 	 * @param int|null   $max_w Image width.
   266 	 * @param int|null $max_h Image height.
   333 	 * @param int|null   $max_h Image height.
   267 	 * @param bool     $crop
   334 	 * @param bool|array $crop  {
       
   335 	 *     Optional. Image cropping behavior. If false, the image will be scaled (default).
       
   336 	 *     If true, image will be cropped to the specified dimensions using center positions.
       
   337 	 *     If an array, the image will be cropped using the array to specify the crop location:
       
   338 	 *
       
   339 	 *     @type string $0 The x crop position. Accepts 'left' 'center', or 'right'.
       
   340 	 *     @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
       
   341 	 * }
   268 	 * @return true|WP_Error
   342 	 * @return true|WP_Error
   269 	 */
   343 	 */
   270 	public function resize( $max_w, $max_h, $crop = false ) {
   344 	public function resize( $max_w, $max_h, $crop = false ) {
   271 		if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) {
   345 		if ( ( $this->size['width'] === $max_w ) && ( $this->size['height'] === $max_h ) ) {
   272 			return true;
   346 			return true;
   273 		}
   347 		}
   274 
   348 
   275 		$dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
   349 		$dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
   276 		if ( ! $dims ) {
   350 		if ( ! $dims ) {
   413 			if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
   487 			if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
   414 				if ( 8 < $this->image->getImageDepth() ) {
   488 				if ( 8 < $this->image->getImageDepth() ) {
   415 					$this->image->setImageDepth( 8 );
   489 					$this->image->setImageDepth( 8 );
   416 				}
   490 				}
   417 			}
   491 			}
   418 
       
   419 			if ( is_callable( array( $this->image, 'setInterlaceScheme' ) ) && defined( 'Imagick::INTERLACE_NO' ) ) {
       
   420 				$this->image->setInterlaceScheme( Imagick::INTERLACE_NO );
       
   421 			}
       
   422 		} catch ( Exception $e ) {
   492 		} catch ( Exception $e ) {
   423 			return new WP_Error( 'image_resize_error', $e->getMessage() );
   493 			return new WP_Error( 'image_resize_error', $e->getMessage() );
   424 		}
   494 		}
   425 	}
   495 	}
   426 
   496 
   445 	 *     maintain aspect ratio according to the provided dimension.
   515 	 *     maintain aspect ratio according to the provided dimension.
   446 	 *
   516 	 *
   447 	 *     @type array ...$0 {
   517 	 *     @type array ...$0 {
   448 	 *         Array of height, width values, and whether to crop.
   518 	 *         Array of height, width values, and whether to crop.
   449 	 *
   519 	 *
   450 	 *         @type int  $width  Image width. Optional if `$height` is specified.
   520 	 *         @type int        $width  Image width. Optional if `$height` is specified.
   451 	 *         @type int  $height Image height. Optional if `$width` is specified.
   521 	 *         @type int        $height Image height. Optional if `$width` is specified.
   452 	 *         @type bool $crop   Optional. Whether to crop the image. Default false.
   522 	 *         @type bool|array $crop   Optional. Whether to crop the image. Default false.
   453 	 *     }
   523 	 *     }
   454 	 * }
   524 	 * }
   455 	 * @return array An array of resized images' metadata by size.
   525 	 * @return array An array of resized images' metadata by size.
   456 	 */
   526 	 */
   457 	public function multi_resize( $sizes ) {
   527 	public function multi_resize( $sizes ) {
   474 	 * @since 5.3.0
   544 	 * @since 5.3.0
   475 	 *
   545 	 *
   476 	 * @param array $size_data {
   546 	 * @param array $size_data {
   477 	 *     Array of size data.
   547 	 *     Array of size data.
   478 	 *
   548 	 *
   479 	 *     @type int  $width  The maximum width in pixels.
   549 	 *     @type int        $width  The maximum width in pixels.
   480 	 *     @type int  $height The maximum height in pixels.
   550 	 *     @type int        $height The maximum height in pixels.
   481 	 *     @type bool $crop   Whether to crop the image to exact dimensions.
   551 	 *     @type bool|array $crop   Whether to crop the image to exact dimensions.
   482 	 * }
   552 	 * }
   483 	 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
   553 	 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
   484 	 *                        WP_Error object on error.
   554 	 *                        WP_Error object on error.
   485 	 */
   555 	 */
   486 	public function make_subsize( $size_data ) {
   556 	public function make_subsize( $size_data ) {
   499 			$size_data['height'] = null;
   569 			$size_data['height'] = null;
   500 		}
   570 		}
   501 
   571 
   502 		if ( ! isset( $size_data['crop'] ) ) {
   572 		if ( ! isset( $size_data['crop'] ) ) {
   503 			$size_data['crop'] = false;
   573 			$size_data['crop'] = false;
       
   574 		}
       
   575 
       
   576 		if ( ( $this->size['width'] === $size_data['width'] ) && ( $this->size['height'] === $size_data['height'] ) ) {
       
   577 			return new WP_Error( 'image_subsize_create_error', __( 'The image already has the requested size.' ) );
   504 		}
   578 		}
   505 
   579 
   506 		$resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
   580 		$resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
   507 
   581 
   508 		if ( is_wp_error( $resized ) ) {
   582 		if ( is_wp_error( $resized ) ) {
   548 		try {
   622 		try {
   549 			$this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
   623 			$this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
   550 			$this->image->setImagePage( $src_w, $src_h, 0, 0 );
   624 			$this->image->setImagePage( $src_w, $src_h, 0, 0 );
   551 
   625 
   552 			if ( $dst_w || $dst_h ) {
   626 			if ( $dst_w || $dst_h ) {
   553 				// If destination width/height isn't specified,
   627 				/*
   554 				// use same as width/height from source.
   628 				 * If destination width/height isn't specified,
       
   629 				 * use same as width/height from source.
       
   630 				 */
   555 				if ( ! $dst_w ) {
   631 				if ( ! $dst_w ) {
   556 					$dst_w = $src_w;
   632 					$dst_w = $src_w;
   557 				}
   633 				}
   558 				if ( ! $dst_h ) {
   634 				if ( ! $dst_h ) {
   559 					$dst_h = $src_h;
   635 					$dst_h = $src_h;
   659 
   735 
   660 	/**
   736 	/**
   661 	 * Saves current image to file.
   737 	 * Saves current image to file.
   662 	 *
   738 	 *
   663 	 * @since 3.5.0
   739 	 * @since 3.5.0
       
   740 	 * @since 6.0.0 The `$filesize` value was added to the returned array.
   664 	 *
   741 	 *
   665 	 * @param string $destfilename Optional. Destination filename. Default null.
   742 	 * @param string $destfilename Optional. Destination filename. Default null.
   666 	 * @param string $mime_type    Optional. The mime-type. Default null.
   743 	 * @param string $mime_type    Optional. The mime-type. Default null.
   667 	 * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
   744 	 * @return array|WP_Error {
       
   745 	 *     Array on success or WP_Error if the file failed to save.
       
   746 	 *
       
   747 	 *     @type string $path      Path to the image file.
       
   748 	 *     @type string $file      Name of the image file.
       
   749 	 *     @type int    $width     Image width.
       
   750 	 *     @type int    $height    Image height.
       
   751 	 *     @type string $mime-type The mime type of the image.
       
   752 	 *     @type int    $filesize  File size of the image.
       
   753 	 * }
   668 	 */
   754 	 */
   669 	public function save( $destfilename = null, $mime_type = null ) {
   755 	public function save( $destfilename = null, $mime_type = null ) {
   670 		$saved = $this->_save( $this->image, $destfilename, $mime_type );
   756 		$saved = $this->_save( $this->image, $destfilename, $mime_type );
   671 
   757 
   672 		if ( ! is_wp_error( $saved ) ) {
   758 		if ( ! is_wp_error( $saved ) ) {
   682 
   768 
   683 		return $saved;
   769 		return $saved;
   684 	}
   770 	}
   685 
   771 
   686 	/**
   772 	/**
       
   773 	 * Removes PDF alpha after it's been read.
       
   774 	 *
       
   775 	 * @since 6.4.0
       
   776 	 */
       
   777 	protected function remove_pdf_alpha_channel() {
       
   778 		$version = Imagick::getVersion();
       
   779 		// Remove alpha channel if possible to avoid black backgrounds for Ghostscript >= 9.14. RemoveAlphaChannel added in ImageMagick 6.7.5.
       
   780 		if ( $version['versionNumber'] >= 0x675 ) {
       
   781 			try {
       
   782 				// Imagick::ALPHACHANNEL_REMOVE mapped to RemoveAlphaChannel in PHP imagick 3.2.0b2.
       
   783 				$this->image->setImageAlphaChannel( defined( 'Imagick::ALPHACHANNEL_REMOVE' ) ? Imagick::ALPHACHANNEL_REMOVE : 12 );
       
   784 			} catch ( Exception $e ) {
       
   785 				return new WP_Error( 'pdf_alpha_process_failed', $e->getMessage() );
       
   786 			}
       
   787 		}
       
   788 	}
       
   789 
       
   790 	/**
       
   791 	 * @since 3.5.0
       
   792 	 * @since 6.0.0 The `$filesize` value was added to the returned array.
       
   793 	 *
   687 	 * @param Imagick $image
   794 	 * @param Imagick $image
   688 	 * @param string  $filename
   795 	 * @param string  $filename
   689 	 * @param string  $mime_type
   796 	 * @param string  $mime_type
   690 	 * @return array|WP_Error
   797 	 * @return array|WP_Error {
       
   798 	 *     Array on success or WP_Error if the file failed to save.
       
   799 	 *
       
   800 	 *     @type string $path      Path to the image file.
       
   801 	 *     @type string $file      Name of the image file.
       
   802 	 *     @type int    $width     Image width.
       
   803 	 *     @type int    $height    Image height.
       
   804 	 *     @type string $mime-type The mime type of the image.
       
   805 	 *     @type int    $filesize  File size of the image.
       
   806 	 * }
   691 	 */
   807 	 */
   692 	protected function _save( $image, $filename = null, $mime_type = null ) {
   808 	protected function _save( $image, $filename = null, $mime_type = null ) {
   693 		list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
   809 		list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
   694 
   810 
   695 		if ( ! $filename ) {
   811 		if ( ! $filename ) {
   701 			$orig_format = $this->image->getImageFormat();
   817 			$orig_format = $this->image->getImageFormat();
   702 
   818 
   703 			$this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
   819 			$this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
   704 		} catch ( Exception $e ) {
   820 		} catch ( Exception $e ) {
   705 			return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
   821 			return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
       
   822 		}
       
   823 
       
   824 		if ( method_exists( $this->image, 'setInterlaceScheme' )
       
   825 			&& method_exists( $this->image, 'getInterlaceScheme' )
       
   826 			&& defined( 'Imagick::INTERLACE_PLANE' )
       
   827 		) {
       
   828 			$orig_interlace = $this->image->getInterlaceScheme();
       
   829 
       
   830 			/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
       
   831 			if ( apply_filters( 'image_save_progressive', false, $mime_type ) ) {
       
   832 				$this->image->setInterlaceScheme( Imagick::INTERLACE_PLANE ); // True - line interlace output.
       
   833 			} else {
       
   834 				$this->image->setInterlaceScheme( Imagick::INTERLACE_NO ); // False - no interlace output.
       
   835 			}
   706 		}
   836 		}
   707 
   837 
   708 		$write_image_result = $this->write_image( $this->image, $filename );
   838 		$write_image_result = $this->write_image( $this->image, $filename );
   709 		if ( is_wp_error( $write_image_result ) ) {
   839 		if ( is_wp_error( $write_image_result ) ) {
   710 			return $write_image_result;
   840 			return $write_image_result;
   711 		}
   841 		}
   712 
   842 
   713 		try {
   843 		try {
   714 			// Reset original format.
   844 			// Reset original format.
   715 			$this->image->setImageFormat( $orig_format );
   845 			$this->image->setImageFormat( $orig_format );
       
   846 
       
   847 			if ( isset( $orig_interlace ) ) {
       
   848 				$this->image->setInterlaceScheme( $orig_interlace );
       
   849 			}
   716 		} catch ( Exception $e ) {
   850 		} catch ( Exception $e ) {
   717 			return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
   851 			return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
   718 		}
   852 		}
   719 
   853 
   720 		// Set correct file permissions.
   854 		// Set correct file permissions.
   881 	 *
  1015 	 *
   882 	 * @return string|WP_Error File to load or WP_Error on failure.
  1016 	 * @return string|WP_Error File to load or WP_Error on failure.
   883 	 */
  1017 	 */
   884 	protected function pdf_setup() {
  1018 	protected function pdf_setup() {
   885 		try {
  1019 		try {
   886 			// By default, PDFs are rendered in a very low resolution.
  1020 			/*
   887 			// We want the thumbnail to be readable, so increase the rendering DPI.
  1021 			 * By default, PDFs are rendered in a very low resolution.
       
  1022 			 * We want the thumbnail to be readable, so increase the rendering DPI.
       
  1023 			 */
   888 			$this->image->setResolution( 128, 128 );
  1024 			$this->image->setResolution( 128, 128 );
   889 
  1025 
   890 			// Only load the first page.
  1026 			// Only load the first page.
   891 			return $this->file . '[0]';
  1027 			return $this->file . '[0]';
   892 		} catch ( Exception $e ) {
  1028 		} catch ( Exception $e ) {
   910 		if ( is_wp_error( $filename ) ) {
  1046 		if ( is_wp_error( $filename ) ) {
   911 			return $filename;
  1047 			return $filename;
   912 		}
  1048 		}
   913 
  1049 
   914 		try {
  1050 		try {
   915 			// When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
  1051 			/*
   916 			// area (resulting in unnecessary whitespace) unless the following option is set.
  1052 			 * When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
       
  1053 			 * area (resulting in unnecessary whitespace) unless the following option is set.
       
  1054 			 */
   917 			$this->image->setOption( 'pdf:use-cropbox', true );
  1055 			$this->image->setOption( 'pdf:use-cropbox', true );
   918 
  1056 
   919 			// Reading image after Imagick instantiation because `setResolution`
  1057 			/*
   920 			// only applies correctly before the image is read.
  1058 			 * Reading image after Imagick instantiation because `setResolution`
       
  1059 			 * only applies correctly before the image is read.
       
  1060 			 */
   921 			$this->image->readImage( $filename );
  1061 			$this->image->readImage( $filename );
   922 		} catch ( Exception $e ) {
  1062 		} catch ( Exception $e ) {
   923 			// Attempt to run `gs` without the `use-cropbox` option. See #48853.
  1063 			// Attempt to run `gs` without the `use-cropbox` option. See #48853.
   924 			$this->image->setOption( 'pdf:use-cropbox', false );
  1064 			$this->image->setOption( 'pdf:use-cropbox', false );
   925 
  1065 
   926 			$this->image->readImage( $filename );
  1066 			$this->image->readImage( $filename );
   927 		}
  1067 		}
   928 
  1068 
   929 		return true;
  1069 		return true;
   930 	}
  1070 	}
   931 
       
   932 }
  1071 }