diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 19b27ba12e2ae..c85e0b1681273 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -118,6 +118,89 @@ public static function supports_mime_type( $mime_type ) { } } + /** + * Checks to see if editor supports saving to the mime-type specified. + * + * @since 7.1.0 + * + * @param string $mime_type + * @return bool + */ + public static function supports_output_mime_type( $mime_type ) { + if ( ! self::supports_mime_type( $mime_type ) ) { + return false; + } + + if ( 0 !== strpos( $mime_type, 'image/' ) ) { + return true; + } + + if ( 'image/avif' !== $mime_type ) { + return true; + } + + return self::supports_encoding_mime_type( $mime_type ); + } + + /** + * Checks whether Imagick can encode a mime type. + * + * Some ImageMagick builds can decode image formats, but cannot encode them. + * + * @since 7.1.0 + * + * @param string $mime_type Image mime type. + * @return bool Whether encoding is supported. + */ + private static function supports_encoding_mime_type( $mime_type ) { + static $supports_encoding = array(); + + if ( isset( $supports_encoding[ $mime_type ] ) ) { + return $supports_encoding[ $mime_type ]; + } + + $extension = strtoupper( self::get_extension( $mime_type ) ); + + if ( ! $extension ) { + $supports_encoding[ $mime_type ] = false; + return false; + } + + $supports_encoding[ $mime_type ] = false; + $image = null; + $temp_file = tempnam( get_temp_dir(), 'wp-image-editor-' ); + + if ( ! $temp_file ) { + return false; + } + + $test_file = $temp_file . '.' . strtolower( $extension ); + + try { + $image = new Imagick(); + $image->newImage( 1, 1, new ImagickPixel( 'white' ) ); + $image->setImageFormat( $extension ); + $supports_encoding[ $mime_type ] = $image->writeImage( $test_file ); + } catch ( Exception $e ) { + $supports_encoding[ $mime_type ] = false; + } + + if ( $image instanceof Imagick ) { + $image->clear(); + $image->destroy(); + } + + if ( file_exists( $temp_file ) ) { + unlink( $temp_file ); + } + + if ( file_exists( $test_file ) ) { + unlink( $test_file ); + } + + return $supports_encoding[ $mime_type ]; + } + /** * Loads image from $this->file into new Imagick Object. * diff --git a/src/wp-includes/class-wp-image-editor.php b/src/wp-includes/class-wp-image-editor.php index d7fe151a0d94a..a05a890fb4c9f 100644 --- a/src/wp-includes/class-wp-image-editor.php +++ b/src/wp-includes/class-wp-image-editor.php @@ -62,6 +62,18 @@ public static function supports_mime_type( $mime_type ) { return false; } + /** + * Checks to see if editor supports saving to the mime-type specified. + * + * @since 7.1.0 + * + * @param string $mime_type + * @return bool + */ + public static function supports_output_mime_type( $mime_type ) { + return static::supports_mime_type( $mime_type ); + } + /** * Loads image from $this->file into editor. * @@ -370,7 +382,7 @@ protected function get_output_format( $filename = null, $mime_type = null ) { $output_format = wp_get_image_editor_output_format( $filename, $mime_type ); if ( isset( $output_format[ $mime_type ] ) - && $this->supports_mime_type( $output_format[ $mime_type ] ) + && $this->supports_output_mime_type( $output_format[ $mime_type ] ) ) { $mime_type = $output_format[ $mime_type ]; $new_ext = $this->get_extension( $mime_type ); @@ -380,7 +392,7 @@ protected function get_output_format( $filename = null, $mime_type = null ) { * Double-check that the mime-type selected is supported by the editor. * If not, choose a default instead. */ - if ( ! $this->supports_mime_type( $mime_type ) ) { + if ( ! $this->supports_output_mime_type( $mime_type ) ) { /** * Filters default mime type prior to getting the file extension. * diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index eb28db81d6ce9..07f5960ffaf13 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4322,6 +4322,15 @@ function _wp_image_editor_choose( $args = array() ) { require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; require_once ABSPATH . WPINC . '/class-avif-info.php'; + + if ( isset( $args['mime_type'] ) && ! isset( $args['output_mime_type'] ) ) { + $default_output_format = wp_get_image_editor_default_output_format(); + + if ( isset( $default_output_format[ $args['mime_type'] ] ) ) { + $args['output_mime_type'] = $default_output_format[ $args['mime_type'] ]; + } + } + /** * Filters the list of image editing library classes. * @@ -4369,18 +4378,22 @@ function _wp_image_editor_choose( $args = array() ) { continue; } - // Implementation should ideally support the output mime type as well if set and different than the passed type. - if ( - isset( $args['mime_type'] ) && - isset( $args['output_mime_type'] ) && - $args['mime_type'] !== $args['output_mime_type'] && - ! call_user_func( array( $implementation, 'supports_mime_type' ), $args['output_mime_type'] ) - ) { - /* - * This implementation supports the input type but not the output type. - * Keep looking to see if we can find an implementation that supports both. - */ - $editor = $implementation; + // Implementation should support saving to the requested output mime type. + if ( isset( $args['mime_type'] ) ) { + $output_mime_type = isset( $args['output_mime_type'] ) ? $args['output_mime_type'] : $args['mime_type']; + $supports_output = is_callable( array( $implementation, 'supports_output_mime_type' ) ) ? + call_user_func( array( $implementation, 'supports_output_mime_type' ), $output_mime_type ) : + call_user_func( array( $implementation, 'supports_mime_type' ), $output_mime_type ); + } + + if ( isset( $args['mime_type'] ) && ! $supports_output ) { + if ( + isset( $args['output_mime_type'] ) && + $args['mime_type'] !== $args['output_mime_type'] + ) { + $editor = $implementation; + } + continue; } @@ -6416,22 +6429,34 @@ function wp_high_priority_element_flag( $value = null ): bool { } /** - * Determines the output format for the image editor. + * Retrieves the default output format mappings for the image editor. * - * @since 6.7.0 + * @since 7.1.0 * @access private * - * @param string $filename Path to the image. - * @param string $mime_type The source image mime type. * @return string[] An array of mime type mappings. */ -function wp_get_image_editor_output_format( $filename, $mime_type ) { - $output_format = array( +function wp_get_image_editor_default_output_format() { + return array( 'image/heic' => 'image/jpeg', 'image/heif' => 'image/jpeg', 'image/heic-sequence' => 'image/jpeg', 'image/heif-sequence' => 'image/jpeg', ); +} + +/** + * Determines the output format for the image editor. + * + * @since 6.7.0 + * @access private + * + * @param string $filename Path to the image. + * @param string $mime_type The source image mime type. + * @return string[] An array of mime type mappings. + */ +function wp_get_image_editor_output_format( $filename, $mime_type ) { + $output_format = wp_get_image_editor_default_output_format(); /** * Filters the image editor output format mapping. diff --git a/tests/phpunit/includes/mock-image-editor.php b/tests/phpunit/includes/mock-image-editor.php index ab2e7379fd439..c47dc2d49616e 100644 --- a/tests/phpunit/includes/mock-image-editor.php +++ b/tests/phpunit/includes/mock-image-editor.php @@ -4,12 +4,14 @@ class WP_Image_Editor_Mock extends WP_Image_Editor { - public static $load_return = true; - public static $test_return = true; - public static $save_return = array(); - public static $spy = array(); - public static $edit_return = array(); - public static $size_return = null; + public static $load_return = true; + public static $test_return = true; + public static $save_return = array(); + public static $spy = array(); + public static $edit_return = array(); + public static $size_return = null; + public static $supports_mime_type_return = true; + public static $supports_output_mime_type_return = true; // Allow testing of jpeg_quality filter. public function set_mime_type( $mime_type = null ) { @@ -23,7 +25,18 @@ public static function test( $args = array() ) { return self::$test_return; } public static function supports_mime_type( $mime_type ) { - return true; + if ( is_array( self::$supports_mime_type_return ) ) { + return ! empty( self::$supports_mime_type_return[ $mime_type ] ); + } + + return self::$supports_mime_type_return; + } + public static function supports_output_mime_type( $mime_type ) { + if ( is_array( self::$supports_output_mime_type_return ) ) { + return ! empty( self::$supports_output_mime_type_return[ $mime_type ] ); + } + + return self::$supports_output_mime_type_return; } public function resize( $max_w, $max_h, $crop = false ) { self::$spy[ __FUNCTION__ ][] = func_get_args(); diff --git a/tests/phpunit/tests/image/editor.php b/tests/phpunit/tests/image/editor.php index 58c9880fe396c..b3b4290fa9994 100644 --- a/tests/phpunit/tests/image/editor.php +++ b/tests/phpunit/tests/image/editor.php @@ -23,6 +23,21 @@ public function set_up() { parent::set_up(); } + public function tear_down() { + WP_Image_Editor_Mock::$load_return = true; + WP_Image_Editor_Mock::$test_return = true; + WP_Image_Editor_Mock::$save_return = array(); + WP_Image_Editor_Mock::$spy = array(); + WP_Image_Editor_Mock::$edit_return = array(); + WP_Image_Editor_Mock::$size_return = null; + WP_Image_Editor_Mock::$supports_mime_type_return = true; + WP_Image_Editor_Mock::$supports_output_mime_type_return = true; + + wp_cache_delete( 'wp_image_editor_choose', 'image_editor' ); + + parent::tear_down(); + } + /** * Test wp_get_image_editor() where load returns true * @@ -49,6 +64,75 @@ public function test_get_editor_load_returns_false() { WP_Image_Editor_Mock::$load_return = true; } + /** + * @ticket 63932 + */ + public function test_get_editor_requires_output_support_for_source_mime_type() { + WP_Image_Editor_Mock::$supports_mime_type_return = array( + 'image/avif' => true, + ); + WP_Image_Editor_Mock::$supports_output_mime_type_return = array( + 'image/avif' => false, + ); + + wp_cache_delete( 'wp_image_editor_choose', 'image_editor' ); + + $this->assertFalse( wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ); + } + + /** + * @ticket 63932 + */ + public function test_get_editor_allows_mapped_output_mime_type() { + WP_Image_Editor_Mock::$supports_mime_type_return = array( + 'image/heic' => true, + ); + WP_Image_Editor_Mock::$supports_output_mime_type_return = array( + 'image/heic' => false, + 'image/jpeg' => true, + ); + + wp_cache_delete( 'wp_image_editor_choose', 'image_editor' ); + + $this->assertTrue( wp_image_editor_supports( array( 'mime_type' => 'image/heic' ) ) ); + } + + /** + * @ticket 63932 + */ + public function test_get_output_format_ignores_unsupported_output_mime_type() { + WP_Image_Editor_Mock::$supports_mime_type_return = array( + 'image/jpeg' => true, + ); + WP_Image_Editor_Mock::$supports_output_mime_type_return = array( + 'image/jpeg' => true, + 'image/avif' => false, + ); + + add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_avif' ) ); + + try { + $editor = wp_get_image_editor( DIR_TESTDATA . '/images/canola.jpg' ); + $this->assertInstanceOf( 'WP_Image_Editor_Mock', $editor ); + + $method = new ReflectionMethod( $editor, 'get_output_format' ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } + + $this->assertSame( + array( + DIR_TESTDATA . '/images/canola.jpg', + 'jpg', + 'image/jpeg', + ), + $method->invoke( $editor, DIR_TESTDATA . '/images/canola.jpg', 'image/jpeg' ) + ); + } finally { + remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_avif' ) ); + } + } + /** * Return integer of 95 for testing. */ @@ -181,6 +265,17 @@ public function image_editor_output_formats( $formats ) { return $formats; } + /** + * Changes the output format for JPEG images to AVIF. + * + * @param array $formats + * @return array + */ + public function image_editor_output_avif( $formats ) { + $formats['image/jpeg'] = 'image/avif'; + return $formats; + } + /** * Changes the quality according to the mime-type. * diff --git a/tests/phpunit/tests/image/editorImagick.php b/tests/phpunit/tests/image/editorImagick.php index e120c32502ad5..9e4da5e819c99 100644 --- a/tests/phpunit/tests/image/editorImagick.php +++ b/tests/phpunit/tests/image/editorImagick.php @@ -705,17 +705,14 @@ static function ( $value ) { * Test filter `image_max_bit_depth` correctly sets the maximum bit depth of resized images. * * @ticket 62285 - * - * Temporarily disabled until we can figure out why it fails on the Trixie based PHP container. - * See https://core.trac.wordpress.org/ticket/63932. - * @requires PHP < 8.3 + * @ticket 63932 */ public function test_image_max_bit_depth() { $file = DIR_TESTDATA . '/images/colors_hdr_p3.avif'; $imagick_image_editor = new WP_Image_Editor_Imagick( $file ); // Skip if AVIF not supported. - if ( ! $imagick_image_editor->supports_mime_type( 'image/avif' ) ) { + if ( ! $imagick_image_editor->supports_output_mime_type( 'image/avif' ) ) { $this->markTestSkipped( 'The image editor does not support the AVIF mime type.' ); } diff --git a/tests/phpunit/tests/image/resize.php b/tests/phpunit/tests/image/resize.php index a7994aa736266..2afec14949a3c 100644 --- a/tests/phpunit/tests/image/resize.php +++ b/tests/phpunit/tests/image/resize.php @@ -87,10 +87,7 @@ public function test_resize_webp() { * Test resizing AVIF image. * * @ticket 51228 - * - * Temporarily disabled until we can figure out why it fails on the Trixie based PHP container. - * See https://core.trac.wordpress.org/ticket/63932. - * @requires PHP < 8.3 + * @ticket 63932 */ public function test_resize_avif() { $file = DIR_TESTDATA . '/images/avif-lossy.avif'; diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 060a7295f6bb4..6221e98fe941f 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -5699,10 +5699,7 @@ public function test_quality_with_image_conversion_file_sizes() { * Test AVIF quality filters. * * @ticket 61614 - * - * Temporarily disabled until we can figure out why it fails on the Trixie based PHP container. - * See https://core.trac.wordpress.org/ticket/63932. - * @requires PHP < 8.3 + * @ticket 63932 */ public function test_quality_with_avif_conversion_file_sizes() { $temp_dir = get_temp_dir(); @@ -5711,7 +5708,7 @@ public function test_quality_with_avif_conversion_file_sizes() { $editor = wp_get_image_editor( $file ); // Only continue if the server supports AVIF. - if ( ! $editor->supports_mime_type( 'image/avif' ) ) { + if ( is_wp_error( $editor ) || ! $editor->supports_output_mime_type( 'image/avif' ) ) { $this->markTestSkipped( 'AVIF is not supported by the selected image editor.' ); }