diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index e80c111dde..f5f9600332 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -37,6 +37,8 @@ add_filter( 'media_upload_library', 'media_upload_library' ); add_filter( 'media_upload_tabs', 'update_gallery_tab' ); +add_filter( 'image_editor_output_format', 'wp_default_image_output_mapping' ); + // Admin color schemes. add_action( 'admin_init', 'register_admin_color_schemes', 1 ); add_action( 'admin_head', 'wp_color_scheme_settings' ); diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index e814fc47e7..9c71e68794 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -917,10 +917,12 @@ function wp_save_image( $post_id ) { } // Save the full-size file, also needed to create sub-sizes. - if ( ! wp_save_image_file( $new_path, $img, $post->post_mime_type, $post_id ) ) { + $saved = wp_save_image_file( $new_path, $img, $post->post_mime_type, $post_id ); + if ( ! $saved ) { $return->error = esc_js( __( 'Unable to save the image.' ) ); return $return; } + $new_path = $saved['path']; if ( 'nothumb' === $target || 'all' === $target || 'full' === $target || $scaled ) { $tag = false; diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 51f21ddb7d..3e2093ac2b 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -3843,3 +3843,18 @@ function wp_media_attach_action( $parent_id, $action = 'attach' ) { exit; } } + +/** + * Filters the default image output mapping. + * + * With this filter callback, WebP image files will be generated for certain JPEG source files. + * + * @since 6.1.0 + * + * @param array $output_mapping Map of mime type to output format. + * @retun array The adjusted default output mapping. + */ +function wp_default_image_output_mapping( $output_mapping ) { + $output_mapping['image/jpeg'] = 'image/webp'; + return $output_mapping; +} diff --git a/src/wp-includes/class-wp-image-editor.php b/src/wp-includes/class-wp-image-editor.php index caa3092d36..15e4aa8d6e 100644 --- a/src/wp-includes/class-wp-image-editor.php +++ b/src/wp-includes/class-wp-image-editor.php @@ -424,19 +424,26 @@ abstract class WP_Image_Editor { return array( $filename, $new_ext, $mime_type ); } - /** - * Builds an output filename based on current file, and adding proper suffix + /** + * Builds an output filename based on current file, and adding proper suffix. * * @since 3.5.0 + * @since 6.1.0 Skips adding a suffix when set to an empty string. When the + * file extension being generated doesn't match the image file extension, + * add the extension to the suffix * - * @param string $suffix - * @param string $dest_path - * @param string $extension - * @return string filename + * @param string $suffix Optional. Suffix to add to the filename. The default null + * will result in a 'widthxheight' suffix. Passing + * an empty string will result in no suffix. + * @param string $dest_path Optional. The path to save the file to. The default null + * will use the image file path. + * @param string $extension Optional. The file extension to use. The default null + * will use the image file extension. + * @return string filename The generated file name. */ public function generate_filename( $suffix = null, $dest_path = null, $extension = null ) { // $suffix will be appended to the destination filename, just before the extension. - if ( ! $suffix ) { + if ( null === $suffix ) { $suffix = $this->get_suffix(); } @@ -457,7 +464,21 @@ abstract class WP_Image_Editor { } } - return trailingslashit( $dir ) . "{$name}-{$suffix}.{$new_ext}"; + if ( empty( $suffix ) ) { + $suffix = ''; + } else { + $suffix = "-{$suffix}"; + } + + // When the file extension being generated doesn't match the image file extension, + // add the extension to the suffix to ensure a unique file name. Prevents + // name conflicts when a single image type can have multiple extensions, + // eg. .jpg, .jpeg and .jpe are all valid JPEG extensions. + if ( ! empty( $extension ) && $extension !== $ext ) { + $suffix .= "-{$ext}"; + } + + return trailingslashit( $dir ) . "{$name}{$suffix}.{$new_ext}"; } /** diff --git a/tests/phpunit/tests/image/editor.php b/tests/phpunit/tests/image/editor.php index 487dad0664..d478dd6bf1 100644 --- a/tests/phpunit/tests/image/editor.php +++ b/tests/phpunit/tests/image/editor.php @@ -18,11 +18,20 @@ class Tests_Image_Editor extends WP_Image_UnitTestCase { require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once DIR_TESTDATA . '/../includes/mock-image-editor.php'; + add_filter( 'image_editor_output_format', '__return_empty_array' ); // This needs to come after the mock image editor class is loaded. parent::set_up(); } + /** + * Tear down the class. + */ + public function tear_down() { + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); + } + /** * Test wp_get_image_editor() where load returns true * @@ -226,10 +235,10 @@ class Tests_Image_Editor extends WP_Image_UnitTestCase { $this->assertSame( trailingslashit( realpath( get_temp_dir() ) ), trailingslashit( realpath( dirname( $editor->generate_filename( null, get_temp_dir() ) ) ) ) ); // Test with a suffix only. - $this->assertSame( 'canola-100x50.png', wp_basename( $editor->generate_filename( null, null, 'png' ) ) ); + $this->assertSame( 'canola-100x50-jpg.png', wp_basename( $editor->generate_filename( null, null, 'png' ) ) ); // Combo! - $this->assertSame( trailingslashit( realpath( get_temp_dir() ) ) . 'canola-new.png', $editor->generate_filename( 'new', realpath( get_temp_dir() ), 'png' ) ); + $this->assertSame( trailingslashit( realpath( get_temp_dir() ) ) . 'canola-new-jpg.png', $editor->generate_filename( 'new', realpath( get_temp_dir() ), 'png' ) ); // Test with a stream destination. $this->assertSame( 'file://testing/path/canola-100x50.jpg', $editor->generate_filename( null, 'file://testing/path' ) ); diff --git a/tests/phpunit/tests/image/editorGd.php b/tests/phpunit/tests/image/editorGd.php index 5d967bdcdb..88dfb80299 100644 --- a/tests/phpunit/tests/image/editorGd.php +++ b/tests/phpunit/tests/image/editorGd.php @@ -17,6 +17,8 @@ class Tests_Image_Editor_GD extends WP_Image_UnitTestCase { require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php'; + add_filter( 'image_editor_output_format', '__return_empty_array' ); + // This needs to come after the mock image editor class is loaded. parent::set_up(); } @@ -30,6 +32,8 @@ class Tests_Image_Editor_GD extends WP_Image_UnitTestCase { $this->remove_added_uploads(); + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); } diff --git a/tests/phpunit/tests/image/editorImagick.php b/tests/phpunit/tests/image/editorImagick.php index 02e0590a2e..a6698ec22c 100644 --- a/tests/phpunit/tests/image/editorImagick.php +++ b/tests/phpunit/tests/image/editorImagick.php @@ -18,6 +18,8 @@ class Tests_Image_Editor_Imagick extends WP_Image_UnitTestCase { require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php'; + add_filter( 'image_editor_output_format', '__return_empty_array' ); + // This needs to come after the mock image editor class is loaded. parent::set_up(); } @@ -31,6 +33,8 @@ class Tests_Image_Editor_Imagick extends WP_Image_UnitTestCase { $this->remove_added_uploads(); + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); } diff --git a/tests/phpunit/tests/image/functions.php b/tests/phpunit/tests/image/functions.php index 86d559145e..75e5514c8b 100644 --- a/tests/phpunit/tests/image/functions.php +++ b/tests/phpunit/tests/image/functions.php @@ -25,6 +25,16 @@ class Tests_Image_Functions extends WP_UnitTestCase { foreach ( glob( $folder ) as $file ) { unlink( $file ); } + + add_filter( 'image_editor_output_format', '__return_empty_array' ); + } + + /** + * Tear down the class. + */ + public function tear_down() { + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); } /** diff --git a/tests/phpunit/tests/image/intermediateSize.php b/tests/phpunit/tests/image/intermediateSize.php index 830359427a..deb72c11e0 100644 --- a/tests/phpunit/tests/image/intermediateSize.php +++ b/tests/phpunit/tests/image/intermediateSize.php @@ -5,6 +5,15 @@ * @group upload */ class Tests_Image_Intermediate_Size extends WP_UnitTestCase { + /** + * Set up the test fixture. + */ + public function set_up() { + add_filter( 'image_editor_output_format', '__return_empty_array' ); + + parent::set_up(); + } + public function tear_down() { $this->remove_added_uploads(); @@ -12,6 +21,9 @@ class Tests_Image_Intermediate_Size extends WP_UnitTestCase { remove_image_size( 'false-height' ); remove_image_size( 'false-width' ); remove_image_size( 'off-by-one' ); + + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); } diff --git a/tests/phpunit/tests/image/resize.php b/tests/phpunit/tests/image/resize.php index 5b302ce295..8c448c842c 100644 --- a/tests/phpunit/tests/image/resize.php +++ b/tests/phpunit/tests/image/resize.php @@ -14,6 +14,15 @@ abstract class WP_Tests_Image_Resize_UnitTestCase extends WP_Image_UnitTestCase parent::set_up(); add_filter( 'wp_image_editors', array( $this, 'wp_image_editors' ) ); + add_filter( 'image_editor_output_format', '__return_empty_array' ); + } + + /** + * Tear down the class. + */ + public function tear_down() { + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); } public function wp_image_editors() { diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index ba4bedf51e..1388e6c210 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -33,7 +33,8 @@ CAP; self::$_sizes = wp_get_additional_image_sizes(); $GLOBALS['_wp_additional_image_sizes'] = array(); - $filename = DIR_TESTDATA . '/images/' . self::$large_filename; + $filename = DIR_TESTDATA . '/images/' . self::$large_filename; + add_filter( 'image_editor_output_format', '__return_empty_array' ); self::$large_id = $factory->attachment->create_upload_object( $filename ); $post_statuses = array( 'publish', 'future', 'draft', 'auto-draft', 'trash' ); @@ -68,6 +69,7 @@ CAP; public static function wpTearDownAfterClass() { $GLOBALS['_wp_additional_image_sizes'] = self::$_sizes; + remove_filter( 'image_editor_output_format', '__return_empty_array' ); } public static function tear_down_after_class() { @@ -3617,6 +3619,59 @@ EOF; // Clean up the above filter. remove_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 ); } + + /** + * Test the wp_default_image_output_mapping function. + * + * @ticket 55443 + */ + public function test_wp_default_image_output_mapping() { + $mapping = wp_default_image_output_mapping( array() ); + $this->assertSame( array( 'image/jpeg' => 'image/webp' ), $mapping ); + } + + /** + * Test that wp_default_image_output_mapping doesn't overwrite existing mappings. + * + * @ticket 55443 + */ + public function test_wp_default_image_output_mapping_existing() { + $mapping = array( 'mime/png' => 'mime/webp' ); + $mapping = wp_default_image_output_mapping( $mapping ); + $this->assertSame( + array( + 'mime/png' => 'mime/webp', + 'image/jpeg' => 'image/webp', + ), + $mapping + ); + } + + /** + * Test that the image editor default output for JPEGs is WebP. + * + * @ticket 55443 + */ + public function test_wp_image_editor_default_output_maps_to_webp() { + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + + $editor = wp_get_image_editor( DIR_TESTDATA . '/images/canola.jpg' ); + $this->assertNotWPError( $editor ); + + $resized = $editor->resize( 100, 100, false ); + $this->assertNotWPError( $resized ); + + $saved = $editor->save(); + $this->assertNotWPError( $saved ); + + if ( $editor->supports_mime_type( 'image/webp' ) ) { + $this->assertSame( 'image/webp', $saved['mime-type'] ); + $this->assertSame( 'canola-100x75-jpg.webp', $saved['file'] ); + } else { + $this->assertSame( 'image/jpeg', $saved['mime-type'] ); + $this->assertSame( 'canola-100x75.jpg', $saved['file'] ); + } + } } /** diff --git a/tests/phpunit/tests/post/attachments.php b/tests/phpunit/tests/post/attachments.php index 2922c185d2..1bbc0c6f79 100644 --- a/tests/phpunit/tests/post/attachments.php +++ b/tests/phpunit/tests/post/attachments.php @@ -6,10 +6,19 @@ * @group upload */ class Tests_Post_Attachments extends WP_UnitTestCase { + /** + * Set up the test fixture. + */ + public function set_up() { + add_filter( 'image_editor_output_format', '__return_empty_array' ); + + parent::set_up(); + } public function tear_down() { // Remove all uploads. $this->remove_added_uploads(); + remove_filter( 'image_editor_output_format', '__return_empty_array' ); parent::tear_down(); } diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 106906ee2b..2c97f81c05 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -93,6 +93,7 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control add_filter( 'rest_pre_dispatch', array( $this, 'wpSetUpBeforeRequest' ), 10, 3 ); add_filter( 'posts_clauses', array( $this, 'save_posts_clauses' ), 10, 2 ); + add_filter( 'image_editor_output_format', '__return_empty_array' ); } public function wpSetUpBeforeRequest( $result ) { @@ -121,6 +122,8 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control WP_Image_Editor_Mock::$size_return = null; } + remove_filter( 'image_editor_output_format', '__return_empty_array' ); + parent::tear_down(); }