wp/wp-includes/class-wp-image-editor-imagick.php
changeset 16 a86126ab1dd4
parent 9 177826044cd9
child 18 be944660c56a
--- a/wp/wp-includes/class-wp-image-editor-imagick.php	Tue Oct 22 16:11:46 2019 +0200
+++ b/wp/wp-includes/class-wp-image-editor-imagick.php	Tue Dec 15 13:49:49 2020 +0100
@@ -23,7 +23,7 @@
 
 	public function __destruct() {
 		if ( $this->image instanceof Imagick ) {
-			// we don't need the original in memory anymore
+			// We don't need the original in memory anymore.
 			$this->image->clear();
 			$this->image->destroy();
 		}
@@ -83,11 +83,6 @@
 			return false;
 		}
 
-		// HHVM Imagick does not support loading from URL, so fail to allow fallback to GD.
-		if ( defined( 'HHVM_VERSION' ) && isset( $args['path'] ) && preg_match( '|^https?://|', $args['path'] ) ) {
-			return false;
-		}
-
 		return true;
 	}
 
@@ -108,11 +103,12 @@
 
 		// setIteratorIndex is optional unless mime is an animated format.
 		// Here, we just say no if you are missing it and aren't loading a jpeg.
-		if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && $mime_type != 'image/jpeg' ) {
+		if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && 'image/jpeg' !== $mime_type ) {
 				return false;
 		}
 
 		try {
+			// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
 			return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
 		} catch ( Exception $e ) {
 			return false;
@@ -146,7 +142,7 @@
 			$file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
 			$filename       = $this->file;
 
-			if ( 'pdf' == $file_extension ) {
+			if ( 'pdf' === $file_extension ) {
 				$filename = $this->pdf_setup();
 			}
 
@@ -158,7 +154,7 @@
 				return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
 			}
 
-			// Select the first frame to handle animated images properly
+			// Select the first frame to handle animated images properly.
 			if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
 				$this->image->setIteratorIndex( 0 );
 			}
@@ -193,7 +189,7 @@
 		}
 
 		try {
-			if ( 'image/jpeg' == $this->mime_type ) {
+			if ( 'image/jpeg' === $this->mime_type ) {
 				$this->image->setImageCompressionQuality( $quality );
 				$this->image->setImageCompression( imagick::COMPRESSION_JPEG );
 			} else {
@@ -213,7 +209,6 @@
 	 *
 	 * @param int $width
 	 * @param int $height
-	 *
 	 * @return true|WP_Error
 	 */
 	protected function update_size( $width = null, $height = null ) {
@@ -246,9 +241,9 @@
 	 *
 	 * @since 3.5.0
 	 *
-	 * @param  int|null $max_w Image width.
-	 * @param  int|null $max_h Image height.
-	 * @param  bool     $crop
+	 * @param int|null $max_w Image width.
+	 * @param int|null $max_h Image height.
+	 * @param bool     $crop
 	 * @return bool|WP_Error
 	 */
 	public function resize( $max_w, $max_h, $crop = false ) {
@@ -260,13 +255,14 @@
 		if ( ! $dims ) {
 			return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
 		}
+
 		list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
 
 		if ( $crop ) {
 			return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
 		}
 
-		// Execute the resize
+		// Execute the resize.
 		$thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
 		if ( is_wp_error( $thumb_result ) ) {
 			return $thumb_result;
@@ -309,10 +305,10 @@
 		);
 
 		/**
-		 * Set the filter value if '$filter_name' name is in our whitelist and the related
-		 * Imagick constant is defined or fall back to our default filter.
+		 * Set the filter value if '$filter_name' name is in the allowed list and the related
+		 * Imagick constant is defined or fall back to the default filter.
 		 */
-		if ( in_array( $filter_name, $allowed_filters ) && defined( 'Imagick::' . $filter_name ) ) {
+		if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
 			$filter = constant( 'Imagick::' . $filter_name );
 		} else {
 			$filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
@@ -361,7 +357,7 @@
 			}
 
 			// Set appropriate quality settings after resizing.
-			if ( 'image/jpeg' == $this->mime_type ) {
+			if ( 'image/jpeg' === $this->mime_type ) {
 				if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
 					$this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
 				}
@@ -408,12 +404,20 @@
 	}
 
 	/**
-	 * Resize multiple images from a single source.
+	 * Create multiple smaller images from a single source.
+	 *
+	 * Attempts to create all sub-sizes and returns the meta data at the end. This
+	 * may result in the server running out of resources. When it fails there may be few
+	 * "orphaned" images left over as the meta data is never returned and saved.
+	 *
+	 * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
+	 * the new images one at a time and allows for the meta data to be saved after
+	 * each new image is created.
 	 *
 	 * @since 3.5.0
 	 *
 	 * @param array $sizes {
-	 *     An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'.
+	 *     An array of image size data arrays.
 	 *
 	 *     Either a height or width must be provided.
 	 *     If one of the two is set to null, the resize will
@@ -430,52 +434,74 @@
 	 * @return array An array of resized images' metadata by size.
 	 */
 	public function multi_resize( $sizes ) {
-		$metadata   = array();
+		$metadata = array();
+
+		foreach ( $sizes as $size => $size_data ) {
+			$meta = $this->make_subsize( $size_data );
+
+			if ( ! is_wp_error( $meta ) ) {
+				$metadata[ $size ] = $meta;
+			}
+		}
+
+		return $metadata;
+	}
+
+	/**
+	 * Create an image sub-size and return the image meta data value for it.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @param array $size_data {
+	 *     Array of size data.
+	 *
+	 *     @type int  $width  The maximum width in pixels.
+	 *     @type int  $height The maximum height in pixels.
+	 *     @type bool $crop   Whether to crop the image to exact dimensions.
+	 * }
+	 * @return array|WP_Error The image data array for inclusion in the `sizes` array in the image meta,
+	 *                        WP_Error object on error.
+	 */
+	public function make_subsize( $size_data ) {
+		if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
+			return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
+		}
+
 		$orig_size  = $this->size;
 		$orig_image = $this->image->getImage();
 
-		foreach ( $sizes as $size => $size_data ) {
-			if ( ! $this->image ) {
-				$this->image = $orig_image->getImage();
-			}
-
-			if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
-				continue;
-			}
-
-			if ( ! isset( $size_data['width'] ) ) {
-				$size_data['width'] = null;
-			}
-			if ( ! isset( $size_data['height'] ) ) {
-				$size_data['height'] = null;
-			}
+		if ( ! isset( $size_data['width'] ) ) {
+			$size_data['width'] = null;
+		}
 
-			if ( ! isset( $size_data['crop'] ) ) {
-				$size_data['crop'] = false;
-			}
-
-			$resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
-			$duplicate     = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
-
-			if ( ! is_wp_error( $resize_result ) && ! $duplicate ) {
-				$resized = $this->_save( $this->image );
+		if ( ! isset( $size_data['height'] ) ) {
+			$size_data['height'] = null;
+		}
 
-				$this->image->clear();
-				$this->image->destroy();
-				$this->image = null;
-
-				if ( ! is_wp_error( $resized ) && $resized ) {
-					unset( $resized['path'] );
-					$metadata[ $size ] = $resized;
-				}
-			}
-
-			$this->size = $orig_size;
+		if ( ! isset( $size_data['crop'] ) ) {
+			$size_data['crop'] = false;
 		}
 
+		$resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
+
+		if ( is_wp_error( $resized ) ) {
+			$saved = $resized;
+		} else {
+			$saved = $this->_save( $this->image );
+
+			$this->image->clear();
+			$this->image->destroy();
+			$this->image = null;
+		}
+
+		$this->size  = $orig_size;
 		$this->image = $orig_image;
 
-		return $metadata;
+		if ( ! is_wp_error( $saved ) ) {
+			unset( $saved['path'] );
+		}
+
+		return $saved;
 	}
 
 	/**
@@ -483,12 +509,12 @@
 	 *
 	 * @since 3.5.0
 	 *
-	 * @param int  $src_x The start x position to crop from.
-	 * @param int  $src_y The start y position to crop from.
-	 * @param int  $src_w The width to crop.
-	 * @param int  $src_h The height to crop.
-	 * @param int  $dst_w Optional. The destination width.
-	 * @param int  $dst_h Optional. The destination height.
+	 * @param int  $src_x   The start x position to crop from.
+	 * @param int  $src_y   The start y position to crop from.
+	 * @param int  $src_w   The width to crop.
+	 * @param int  $src_h   The height to crop.
+	 * @param int  $dst_w   Optional. The destination width.
+	 * @param int  $dst_h   Optional. The destination height.
 	 * @param bool $src_abs Optional. If the source crop points are absolute.
 	 * @return bool|WP_Error
 	 */
@@ -503,8 +529,8 @@
 			$this->image->setImagePage( $src_w, $src_h, 0, 0 );
 
 			if ( $dst_w || $dst_h ) {
-				// If destination width/height isn't specified, use same as
-				// width/height from source.
+				// If destination width/height isn't specified,
+				// use same as width/height from source.
 				if ( ! $dst_w ) {
 					$dst_w = $src_w;
 				}
@@ -541,7 +567,7 @@
 		try {
 			$this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
 
-			// Normalise Exif orientation data so that display is consistent across devices.
+			// Normalise EXIF orientation data so that display is consistent across devices.
 			if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
 				$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
 			}
@@ -577,13 +603,38 @@
 			if ( $vert ) {
 				$this->image->flopImage();
 			}
+
+			// Normalise EXIF orientation data so that display is consistent across devices.
+			if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
+				$this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
+			}
 		} catch ( Exception $e ) {
 			return new WP_Error( 'image_flip_error', $e->getMessage() );
 		}
+
 		return true;
 	}
 
 	/**
+	 * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
+	 *
+	 * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
+	 * if EXIF Orientation can be reset afterwards.
+	 *
+	 * @since 5.3.0
+	 *
+	 * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
+	 *                       WP_Error if error while rotating.
+	 */
+	public function maybe_exif_rotate() {
+		if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
+			return parent::maybe_exif_rotate();
+		} else {
+			return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
+		}
+	}
+
+	/**
 	 * Saves current image to file.
 	 *
 	 * @since 3.5.0
@@ -611,8 +662,8 @@
 
 	/**
 	 * @param Imagick $image
-	 * @param string $filename
-	 * @param string $mime_type
+	 * @param string  $filename
+	 * @param string  $mime_type
 	 * @return array|WP_Error
 	 */
 	protected function _save( $image, $filename = null, $mime_type = null ) {
@@ -623,26 +674,26 @@
 		}
 
 		try {
-			// Store initial Format
+			// Store initial format.
 			$orig_format = $this->image->getImageFormat();
 
 			$this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
 			$this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) );
 
-			// Reset original Format
+			// Reset original format.
 			$this->image->setImageFormat( $orig_format );
 		} catch ( Exception $e ) {
 			return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
 		}
 
-		// Set correct file permissions
+		// Set correct file permissions.
 		$stat  = stat( dirname( $filename ) );
-		$perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
-		@ chmod( $filename, $perms );
+		$perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits.
+		chmod( $filename, $perms );
 
-		/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
 		return array(
 			'path'      => $filename,
+			/** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
 			'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
 			'width'     => $this->size['width'],
 			'height'    => $this->size['height'],
@@ -662,14 +713,14 @@
 		list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
 
 		try {
-			// Temporarily change format for stream
+			// Temporarily change format for stream.
 			$this->image->setImageFormat( strtoupper( $extension ) );
 
-			// Output stream of image content
+			// Output stream of image content.
 			header( "Content-Type: $mime_type" );
 			print $this->image->getImageBlob();
 
-			// Reset Image to original Format
+			// Reset image to original format.
 			$this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
 		} catch ( Exception $e ) {
 			return new WP_Error( 'image_stream_error', $e->getMessage() );
@@ -688,12 +739,12 @@
 	protected function strip_meta() {
 
 		if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
-			/* translators: %s: ImageMagick method name */
+			/* translators: %s: ImageMagick method name. */
 			return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::getImageProfiles()</code>' ) );
 		}
 
 		if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
-			/* translators: %s: ImageMagick method name */
+			/* translators: %s: ImageMagick method name. */
 			return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::removeImageProfile()</code>' ) );
 		}
 
@@ -717,7 +768,7 @@
 		try {
 			// Strip profiles.
 			foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
-				if ( ! in_array( $key, $protected_profiles ) ) {
+				if ( ! in_array( $key, $protected_profiles, true ) ) {
 					$this->image->removeImageProfile( $key );
 				}
 			}
@@ -742,6 +793,10 @@
 			// We want the thumbnail to be readable, so increase the rendering DPI.
 			$this->image->setResolution( 128, 128 );
 
+			// When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
+			// area (resulting in unnecessary whitespace) unless the following option is set.
+			$this->image->setOption( 'pdf:use-cropbox', true );
+
 			// Only load the first page.
 			return $this->file . '[0]';
 		} catch ( Exception $e ) {