Media: enable generating multiple mime types for image uploads; specifically WebP versions for JPEG images by default.

This changeset adds the capability for core media uploads to generate sub sized images in more than a single mime type. The output formats for each mime type can be controlled through a filter. WebP is used as an additional output format for JPEG images by default to improve front end performance.

When generating additional mime types, only images which are smaller than the respective original are retained. By default, additional mime type images are only generated for the built-in core image sizes and any custom sizes that have opted in.

Image meta is updated with a new 'sources' array containing file details for each mime type. Each image size in the 'sizes' array also gets a new 'sources' array that contains the image file details for each mime type.

This change also increases image upload retries to accommodate additional image sizes. It also adds a `$mime_type` parameter to the `wp_get_missing_image_subsizes` function and filter.

This change adds three new filters to enable full control of secondary mime image generation and output:

* A new filter `wp_image_sizes_with_additional_mime_type_support` that filters the sizes that support secondary mime type output. Developers can use this to control the output of additional mime type sub-sized images on a per size basis.
* A new filter `wp_upload_image_mime_transforms` that filters the output mime types for a given input mime type. Developers can use this to control generation of additional mime types for a given input mime type or even override the original mime type.
* A new filter `wp_content_image_mimes` which controls image mime type output selection and order for frontend content. Developers can use this to control the mime type output preference order for content images. Content images inserted from the media library will use the available image versions based on the order from this filter.

Thanks to the many contributors who helped develop, test and give feedback on this feature.

A haiku to summarize:

Upload a JPEG
Images of all sizes
Output as WebPs

Props flixos90, MatthiasReinholz, studiolxv, markhowellsmead, eatingrules, pbiron, mukesh27, joegrainger, mehulkaklotar, tweetythierry, akshitsethi, peterwilsoncc, eugenemanuilov, mitogh, shetheliving, clarkeemily, codekraft, mikeschroder, clorith, kasparsd, spacedmonkey, trevorpfromsandee, jb510, scofennellgmailcom, seedsca, cagsmith, karinclimber, dainemawer, baxbridge, grapplerulrich, sobatkras, chynnabenton, tonylocalword, barneydavey, kwillmorth, garymatthews919, olliejones, imarkinteractive, jeffpaul, feastdesignco, webbeetle, masteradhoc.

See #55443.



git-svn-id: https://develop.svn.wordpress.org/trunk@53751 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Adam Silverstein
2022-07-21 18:01:01 +00:00
parent 0a9e88bafc
commit 9bbf3985e3
12 changed files with 1791 additions and 128 deletions
+185
View File
@@ -2251,11 +2251,14 @@ EOF;
// Do not add width, height, and loading.
add_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' );
add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $content_filtered, wp_filter_content_tags( $content_unfiltered ) );
remove_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' );
remove_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -2289,9 +2292,12 @@ EOF;
$img = wp_img_tag_add_loading_attr( $img, 'test' );
$img = wp_img_tag_add_decoding_attr( $img, 'the_content' );
$img = preg_replace( '|<img ([^>]+) />|', '<img $1 ' . 'srcset="image2x.jpg 2x" />', $img );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
// The content filter should return the image unchanged.
$this->assertSame( $img, wp_filter_content_tags( $img ) );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -2361,6 +2367,7 @@ EOF;
add_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' );
add_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
add_filter( 'wp_img_tag_add_decoding_attr', '__return_false' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
add_filter(
'wp_content_img_tag',
@@ -2423,6 +2430,7 @@ EOF;
* @requires function imagejpeg
*/
public function test_wp_filter_content_tags_schemes() {
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$image_meta = wp_get_attachment_metadata( self::$large_id );
$size_array = $this->get_image_size_array_from_meta( $image_meta, 'medium' );
@@ -2468,6 +2476,7 @@ EOF;
$actual = wp_filter_content_tags( $unfiltered );
$this->assertSame( $expected, $actual );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -2961,11 +2970,13 @@ EOF;
// Do not add loading, srcset, and sizes.
add_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
add_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $content_filtered, wp_filter_content_tags( $content_unfiltered ) );
remove_filter( 'wp_img_tag_add_loading_attr', '__return_false' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -3041,11 +3052,13 @@ EOF;
// Do not add width, height, srcset, and sizes.
add_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' );
add_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $content_filtered, wp_filter_content_tags( $content_unfiltered ) );
remove_filter( 'wp_img_tag_add_width_and_height_attr', '__return_false' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -3074,9 +3087,13 @@ EOF;
// Enable globally for all tags.
add_filter( 'wp_lazy_loading_enabled', '__return_true' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $content_filtered, wp_filter_content_tags( $content_unfiltered ) );
remove_filter( 'wp_lazy_loading_enabled', '__return_true' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -3101,9 +3118,12 @@ EOF;
// Disable globally for all tags.
add_filter( 'wp_lazy_loading_enabled', '__return_false' );
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $content, wp_filter_content_tags( $content ) );
remove_filter( 'wp_lazy_loading_enabled', '__return_false' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
}
/**
@@ -3529,6 +3549,7 @@ EOF;
*/
function test_wp_filter_content_tags_with_wp_get_loading_attr_default() {
global $wp_query, $wp_the_query;
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$img1 = get_image_tag( self::$large_id, '', '', '', 'large' );
$iframe1 = '<iframe src="https://www.example.com" width="640" height="360"></iframe>';
@@ -3564,6 +3585,7 @@ EOF;
$content_filtered = wp_filter_content_tags( $content_unfiltered, 'the_content' );
remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
}
remove_filter( 'wp_content_image_mimes', '__return_empty_array' );
// After filtering, the first image should not be lazy-loaded while the other ones should be.
$this->assertSame( $content_expected, $content_filtered );
@@ -3613,6 +3635,169 @@ EOF;
// Clean up the above filter.
remove_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 );
}
/**
* @ticket 55443
*/
public function test_wp_image_use_alternate_mime_types_replaces_jpg_with_webp_where_available() {
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
$this->markTestSkipped( 'This test requires WebP support.' );
}
// The attachment $large_id is a JPEG image, so it gets WebP files generated by default.
$tag = wp_get_attachment_image( self::$large_id, 'full' );
$expected_tag = $tag;
$metadata = wp_get_attachment_metadata( self::$large_id );
foreach ( $metadata['sizes'] as $size => $properties ) {
// Some sizes may not have WebP if the WebP file is larger than the JPEG for the size.
if ( ! isset( $properties['sources']['image/webp'] ) ) {
continue;
}
$expected_tag = str_replace( $properties['sources']['image/jpeg']['file'], $properties['sources']['image/webp']['file'], $expected_tag );
}
// Same applies to the full size.
if ( isset( $metadata['sources']['image/webp'] ) ) {
$expected_tag = str_replace( $metadata['sources']['image/jpeg']['file'], $metadata['sources']['image/webp']['file'], $expected_tag );
}
$this->assertNotSame( $tag, $expected_tag );
$this->assertSame( $expected_tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
}
/**
* @ticket 55443
*/
public function test_wp_image_use_alternate_mime_types_does_not_replace_jpg_when_webp_is_not_available() {
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
$this->markTestSkipped( 'This test requires WebP support.' );
}
// The attachment $large_id is a JPEG image, so it gets WebP files generated by default.
$tag = wp_get_attachment_image( self::$large_id, 'full' );
// Update attachment metadata as if the image had no WebP available for any sub-sizes and the full size.
$metadata = wp_get_attachment_metadata( self::$large_id );
foreach ( $metadata['sizes'] as $size => $properties ) {
unset( $metadata['sizes'][ $size ]['sources']['image/webp'] );
}
unset( $metadata['sources']['image/webp'] );
wp_update_attachment_metadata( self::$large_id, $metadata );
$this->assertSame( $tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
}
/**
* @ticket 55443
*/
public function test_wp_image_use_alternate_mime_types_still_replaces_jpg_subsizes_when_webp_is_not_available_for_full_size() {
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
$this->markTestSkipped( 'This test requires WebP support.' );
}
// The attachment $large_id is a JPEG image, so it gets WebP files generated by default.
$tag = wp_get_attachment_image( self::$large_id, 'full' );
$expected_tag = $tag;
// Update attachment metadata as if the image had no WebP available for the full size.
$metadata = wp_get_attachment_metadata( self::$large_id );
unset( $metadata['sources']['image/webp'] );
wp_update_attachment_metadata( self::$large_id, $metadata );
foreach ( $metadata['sizes'] as $size => $properties ) {
// Some sizes may not have WebP if the WebP file is larger than the JPEG for the size.
if ( ! isset( $properties['sources']['image/webp'] ) ) {
continue;
}
$expected_tag = str_replace( $properties['sources']['image/jpeg']['file'], $properties['sources']['image/webp']['file'], $expected_tag );
}
$this->assertNotSame( $tag, $expected_tag );
$this->assertSame( $expected_tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
}
/**
* @ticket 55443
*/
public function test_wp_image_use_alternate_mime_types_respects_wp_content_image_mimes_filter() {
if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/webp' ) ) ) {
$this->markTestSkipped( 'This test requires WebP support.' );
}
// The attachment $large_id is a JPEG image, so it gets WebP files generated by default.
$tag = wp_get_attachment_image( self::$large_id, 'full' );
// Invalid filter value results in no changes to content.
add_filter( 'wp_content_image_mimes', '__return_false' );
$this->assertSame( $tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
// Empty array results in no changes to content.
add_filter( 'wp_content_image_mimes', '__return_empty_array' );
$this->assertSame( $tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
// Preferring JPEG over WebP results in no changes to content.
add_filter(
'wp_content_image_mimes',
function() {
return array( 'image/jpeg', 'image/webp' );
}
);
$this->assertSame( $tag, wp_image_use_alternate_mime_types( $tag, 'the_content', self::$large_id ) );
}
/**
* @ticket 55443
*/
public function test__wp_in_front_end_context_without_wp_query() {
unset( $GLOBALS['wp_query'] );
$this->assertFalse( _wp_in_front_end_context() );
}
/**
* @ticket 55443
*/
public function test__wp_in_front_end_context_with_feed() {
remove_all_actions( 'template_redirect' );
do_action( 'template_redirect' );
$GLOBALS['wp_query']->is_feed = true;
$this->assertFalse( _wp_in_front_end_context() );
}
/**
* @ticket 55443
*/
public function test__wp_in_front_end_context_before_and_after_template_redirect() {
$result = _wp_in_front_end_context();
remove_all_actions( 'template_redirect' );
do_action( 'template_redirect' );
$this->assertFalse( $result );
$this->assertTrue( _wp_in_front_end_context() );
}
/**
* @ticket 55443
*/
public function test__wp_in_front_end_context_within_wp_head() {
remove_all_actions( 'template_redirect' );
do_action( 'template_redirect' );
// Call function within a 'wp_head' callback.
remove_all_actions( 'wp_head' );
$result = null;
add_action(
'wp_head',
function() use ( &$result ) {
$result = _wp_in_front_end_context();
}
);
do_action( 'wp_head' );
$this->assertFalse( $result );
}
}
/**