diff -r 34716fd837a4 -r be944660c56a wp/wp-includes/class-wp-image-editor-imagick.php --- a/wp/wp-includes/class-wp-image-editor-imagick.php Tue Dec 15 15:52:01 2020 +0100 +++ b/wp/wp-includes/class-wp-image-editor-imagick.php Wed Sep 21 18:19:35 2022 +0200 @@ -71,6 +71,7 @@ 'flipimage', 'flopimage', 'readimage', + 'readimageblob', ); // Now, test for deep requirements within Imagick. @@ -127,7 +128,7 @@ return true; } - if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) { + if ( ! is_file( $this->file ) && ! wp_is_stream( $this->file ) ) { return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file ); } @@ -140,15 +141,21 @@ try { $this->image = new Imagick(); $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) ); - $filename = $this->file; if ( 'pdf' === $file_extension ) { - $filename = $this->pdf_setup(); - } + $pdf_loaded = $this->pdf_load_source(); - // Reading image after Imagick instantiation because `setResolution` - // only applies correctly before the image is read. - $this->image->readImage( $filename ); + if ( is_wp_error( $pdf_loaded ) ) { + return $pdf_loaded; + } + } else { + if ( wp_is_stream( $this->file ) ) { + // Due to reports of issues with streams with `Imagick::readImageFile()`, uses `Imagick::readImageBlob()` instead. + $this->image->readImageBlob( file_get_contents( $this->file ), $this->file ); + } else { + $this->image->readImage( $this->file ); + } + } if ( ! $this->image->valid() ) { return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); @@ -165,6 +172,7 @@ } $updated_size = $this->update_size(); + if ( is_wp_error( $updated_size ) ) { return $updated_size; } @@ -189,19 +197,32 @@ } try { - if ( 'image/jpeg' === $this->mime_type ) { - $this->image->setImageCompressionQuality( $quality ); - $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); - } else { - $this->image->setImageCompressionQuality( $quality ); + switch ( $this->mime_type ) { + case 'image/jpeg': + $this->image->setImageCompressionQuality( $quality ); + $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); + break; + case 'image/webp': + $webp_info = wp_get_webp_info( $this->file ); + + if ( 'lossless' === $webp_info['type'] ) { + // Use WebP lossless settings. + $this->image->setImageCompressionQuality( 100 ); + $this->image->setOption( 'webp:lossless', 'true' ); + } else { + $this->image->setImageCompressionQuality( $quality ); + } + break; + default: + $this->image->setImageCompressionQuality( $quality ); } } catch ( Exception $e ) { return new WP_Error( 'image_quality_error', $e->getMessage() ); } - return true; } + /** * Sets or updates current image size. * @@ -244,7 +265,7 @@ * @param int|null $max_w Image width. * @param int|null $max_h Image height. * @param bool $crop - * @return bool|WP_Error + * @return true|WP_Error */ public function resize( $max_w, $max_h, $crop = false ) { if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) { @@ -283,7 +304,7 @@ * @param int $dst_h The destination height. * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'. * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true. - * @return bool|WP_Error + * @return void|WP_Error */ protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) { $allowed_filters = array( @@ -516,7 +537,7 @@ * @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 + * @return true|WP_Error */ public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) { if ( $src_abs ) { @@ -548,6 +569,7 @@ } catch ( Exception $e ) { return new WP_Error( 'image_crop_error', $e->getMessage() ); } + return $this->update_size(); } @@ -582,6 +604,7 @@ } catch ( Exception $e ) { return new WP_Error( 'image_rotate_error', $e->getMessage() ); } + return true; } @@ -678,8 +701,16 @@ $orig_format = $this->image->getImageFormat(); $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) ); - $this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) ); + } catch ( Exception $e ) { + return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); + } + $write_image_result = $this->write_image( $this->image, $filename ); + if ( is_wp_error( $write_image_result ) ) { + return $write_image_result; + } + + try { // Reset original format. $this->image->setImageFormat( $orig_format ); } catch ( Exception $e ) { @@ -702,12 +733,62 @@ } /** + * Writes an image to a file or stream. + * + * @since 5.6.0 + * + * @param Imagick $image + * @param string $filename The destination filename or stream URL. + * @return true|WP_Error + */ + private function write_image( $image, $filename ) { + if ( wp_is_stream( $filename ) ) { + /* + * Due to reports of issues with streams with `Imagick::writeImageFile()` and `Imagick::writeImage()`, copies the blob instead. + * Checks for exact type due to: https://www.php.net/manual/en/function.file-put-contents.php + */ + if ( file_put_contents( $filename, $image->getImageBlob() ) === false ) { + return new WP_Error( + 'image_save_error', + sprintf( + /* translators: %s: PHP function name. */ + __( '%s failed while writing image to stream.' ), + 'file_put_contents()' + ), + $filename + ); + } else { + return true; + } + } else { + $dirname = dirname( $filename ); + + if ( ! wp_mkdir_p( $dirname ) ) { + return new WP_Error( + 'image_save_error', + sprintf( + /* translators: %s: Directory path. */ + __( 'Unable to create directory %s. Is its parent directory writable by the server?' ), + esc_html( $dirname ) + ) + ); + } + + try { + return $image->writeImage( $filename ); + } catch ( Exception $e ) { + return new WP_Error( 'image_save_error', $e->getMessage(), $filename ); + } + } + } + + /** * Streams current image to browser. * * @since 3.5.0 * * @param string $mime_type The mime type of the image. - * @return bool|WP_Error True on success, WP_Error object on failure. + * @return true|WP_Error True on success, WP_Error object on failure. */ public function stream( $mime_type = null ) { list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type ); @@ -739,13 +820,25 @@ protected function strip_meta() { if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) { - /* translators: %s: ImageMagick method name. */ - return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), 'Imagick::getImageProfiles()' ) ); + return new WP_Error( + 'image_strip_meta_error', + sprintf( + /* translators: %s: ImageMagick method name. */ + __( '%s is required to strip image meta.' ), + 'Imagick::getImageProfiles()' + ) + ); } if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) { - /* translators: %s: ImageMagick method name. */ - return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), 'Imagick::removeImageProfile()' ) ); + return new WP_Error( + 'image_strip_meta_error', + sprintf( + /* translators: %s: ImageMagick method name. */ + __( '%s is required to strip image meta.' ), + 'Imagick::removeImageProfile()' + ) + ); } /* @@ -793,10 +886,6 @@ // 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 ) { @@ -804,4 +893,39 @@ } } + /** + * Load the image produced by Ghostscript. + * + * Includes a workaround for a bug in Ghostscript 8.70 that prevents processing of some PDF files + * when `use-cropbox` is set. + * + * @since 5.6.0 + * + * @return true|WP_error + */ + protected function pdf_load_source() { + $filename = $this->pdf_setup(); + + if ( is_wp_error( $filename ) ) { + return $filename; + } + + try { + // 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 ); + + // Reading image after Imagick instantiation because `setResolution` + // only applies correctly before the image is read. + $this->image->readImage( $filename ); + } catch ( Exception $e ) { + // Attempt to run `gs` without the `use-cropbox` option. See #48853. + $this->image->setOption( 'pdf:use-cropbox', false ); + + $this->image->readImage( $filename ); + } + + return true; + } + }