diff -r 3d4e9c994f10 -r a86126ab1dd4 wp/wp-includes/class-wp-image-editor-imagick.php --- 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.' ), 'Imagick::getImageProfiles()' ) ); } 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.' ), 'Imagick::removeImageProfile()' ) ); } @@ -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 ) {