Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
36cd431
Add the initial version of webp-hero script.
eugene-manuilov May 24, 2022
11197de
Fix formatting issues.
eugene-manuilov May 24, 2022
8262fca
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jun 9, 2022
e06409b
Update webp-hero hook to be a fallback.
eugene-manuilov Jun 9, 2022
6637709
Add mime types trasform checks to make sure we add the fallback if jp…
eugene-manuilov Jun 10, 2022
5cb1283
Updated the fallback script to process images that was added while we…
eugene-manuilov Jun 10, 2022
7044a6d
Updated the fallback script to use REST API to get image details.
eugene-manuilov Jun 10, 2022
45a5fde
Updated the fallback script to pull media details in batches.
eugene-manuilov Jun 10, 2022
0d91c53
Fix js issues that prevents images to work in IE9.
eugene-manuilov Jun 10, 2022
6ffde35
Address code review feedback.
eugene-manuilov Jun 13, 2022
829390a
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jun 15, 2022
9b280a6
Address code review feedback.
eugene-manuilov Jun 15, 2022
5365f64
Removed unnecessary check.
eugene-manuilov Jun 15, 2022
d97c430
Address code review feedback.
eugene-manuilov Jun 22, 2022
4fbb95e
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jun 22, 2022
49fca5e
Update the condition to register fallback hook only if it hasn't been…
eugene-manuilov Jun 22, 2022
e44a8a2
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jun 24, 2022
4db66ba
Add phpunit tests.
eugene-manuilov Jun 24, 2022
74fe886
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jul 7, 2022
f205815
Address code review feedback.
eugene-manuilov Jul 8, 2022
ea38eae
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jul 8, 2022
a1fb857
Avoid JS error in case JPEG source does not exist for an image.
felixarntz Jul 12, 2022
a9bf111
Make printing inline script tag more defensive.
felixarntz Jul 12, 2022
6871972
Use get_echo() utility in tests.
Jul 12, 2022
9429f1b
Fix syntax error :D
Jul 12, 2022
696ade3
Merge remote-tracking branch 'origin/trunk' into enhancement/341-wepb…
eugene-manuilov Jul 13, 2022
7e70d49
Merge branch 'enhancement/341-wepb-hero' of github.com:WordPress/perf…
eugene-manuilov Jul 13, 2022
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions modules/images/webp-uploads/fallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
window.wpPerfLab = window.wpPerfLab || {};

( function( document ) {
window.wpPerfLab.webpUploadsFallbackWebpImages = function( media ) {
for ( var i = 0; i < media.length; i++ ) {
try {
if ( ! media[i].media_details.sources || ! media[i].media_details.sources['image/jpeg'] ) {
continue;
}

var ext = media[i].media_details.sources['image/jpeg'].file.match( /\.\w+$/i );
if ( ! ext || ! ext[0] ) {
continue;
}

var images = document.querySelectorAll( 'img.wp-image-' + media[i].id );
for ( var j = 0; j < images.length; j++ ) {
images[j].src = images[j].src.replace( /\.webp$/i, ext[0] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the file name always match?

The thought behind the question: Someone uploads the image hamilton-the-musical.jpg, this will generate fallback image hamilton-the-musical.webp. Later the image hamilton-the-musical.jpeg is uploaded (note the e), this will generate images of a different name for the fallback -- probably hamilton-the-musical-2.webp.

🔢 The same question applies for srcset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a good point but I think it is out of scope for this issue. The problem is bigger that just properly replacing webp files here. I tried to replicate your edge case and it looks like in this case webp images for the second image will override webp images for the first image 🤔.

I uploaded 20180607_144302.jpg image first, webp images have been created correctly. Then I uploaded the same image but with another extension 20180607_144302.jpeg and in this time webp images overriden webp images generated for the first image. Here is what I got on the server:

-rw-r--r-- 1 www-data www-data 161891 Jul  8 18:15 20180607_144302-1024x576.jpeg
-rw-r--r-- 1 www-data www-data 161891 Jul  8 18:13 20180607_144302-1024x576.jpg
-rw-r--r-- 1 www-data www-data 170276 Jul  8 18:14 20180607_144302-1024x576.webp
-rw-r--r-- 1 www-data www-data   8220 Jul  8 18:15 20180607_144302-150x150.jpeg
-rw-r--r-- 1 www-data www-data   8220 Jul  8 18:13 20180607_144302-150x150.jpg
-rw-r--r-- 1 www-data www-data   8724 Jul  8 18:14 20180607_144302-150x150.webp
-rw-r--r-- 1 www-data www-data 330858 Jul  8 18:15 20180607_144302-1536x864.jpeg
-rw-r--r-- 1 www-data www-data 330858 Jul  8 18:14 20180607_144302-1536x864.jpg
-rw-r--r-- 1 www-data www-data 331106 Jul  8 18:14 20180607_144302-1536x864.webp
-rw-r--r-- 1 www-data www-data 529186 Jul  8 18:15 20180607_144302-2048x1152.jpeg
-rw-r--r-- 1 www-data www-data 529186 Jul  8 18:14 20180607_144302-2048x1152.jpg
-rw-r--r-- 1 www-data www-data 506556 Jul  8 18:14 20180607_144302-2048x1152.webp
-rw-r--r-- 1 www-data www-data  17533 Jul  8 18:15 20180607_144302-300x169.jpeg
-rw-r--r-- 1 www-data www-data  17533 Jul  8 18:13 20180607_144302-300x169.jpg
-rw-r--r-- 1 www-data www-data  18692 Jul  8 18:14 20180607_144302-300x169.webp
-rw-r--r-- 1 www-data www-data  96605 Jul  8 18:15 20180607_144302-768x432.jpeg
-rw-r--r-- 1 www-data www-data  96605 Jul  8 18:14 20180607_144302-768x432.jpg
-rw-r--r-- 1 www-data www-data 102924 Jul  8 18:14 20180607_144302-768x432.webp
-rw-r--r-- 1 www-data www-data 934789 Jul  8 18:15 20180607_144302.jpeg
-rw-r--r-- 1 www-data www-data 934789 Jul  8 18:13 20180607_144302.jpg
-rw-r--r-- 1 www-data www-data 826980 Jul  8 18:14 20180607_144302.webp

This doesn't look right although this is a very unlikely edge case. I think it is worth creating a separate issue for it 🤔.

cc @felixarntz @adamsilverstein

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was previously discussed and should have been fixed in the plugin - see #358 - not sure why you still see this happening. I'm not sure this fix is great though, it skips the duplicate generation; instead behavior should be like or use wp_unique_filename.

For core I am working on including this in the core patch as well, I tried one approach in 93542af (#2393) - that isn't quite finished - note there can actually be three variations - cat.jpe, cat.jpeg, and cat.jpg - yes ".jpe" is also valid!

Another option would be to incorporate the fix into WP_Image_Editor::generate_filename or save - again the tricky part is making sure overwriting works when you want it to (ie. regenerate images).

Copy link
Contributor

@peterwilsoncc peterwilsoncc Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a good point but I think it is out of scope for this issue. The problem is bigger that just properly replacing webp files here.

Thanks for testing this. I'm happy if y'all decide it is out of scope for this ticket. It seems like a much bigger problem than I thought when asking the question :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem of avoiding the override was indeed fixed in #358 - however that shouldn't have affected the PR here anyway, since realistically the problem is only on the JPEG (jpeg/jpg/jpe) side. Now that #358 is in place, no conflicting WebP image would be created so we should be good for now.

With that said, I agree this is a valid point, and I think a much more stable implementation for the replacement would be to replace the full file name for the image/webp version per size with its corresponding full file name for the image/jpeg version. Let's open a follow-up issue to add that.

var srcset = images[j].getAttribute( 'srcset' );
if ( srcset ) {
images[j].setAttribute( 'srcset', srcset.replace( /\.webp(\s)/ig, ext[0] + '$1' ) );
}
}
} catch ( e ) {
}
}
};

var restApi = document.getElementById( 'webpUploadsFallbackWebpImages' ).getAttribute( 'data-rest-api' );

var loadMediaDetails = function( nodes ) {
var ids = [];
for ( var i = 0; i < nodes.length; i++ ) {
var node = nodes[i];
var srcset = node.getAttribute( 'srcset' ) || '';

if (
node.nodeName !== "IMG" ||
( ! node.src.match( /\.webp$/i ) && ! srcset.match( /\.webp\s+/ ) )
) {
continue;
}

var attachment = node.className.match( /wp-image-(\d+)/i );
if ( attachment && attachment[1] && ids.indexOf( attachment[1] ) === -1 ) {
ids.push( attachment[1] );
}
}

for ( var page = 0, pages = Math.ceil( ids.length / 100 ); page < pages; page++ ) {
var pageIds = [];
for ( var i = 0; i < 100 && i + page * 100 < ids.length; i++ ) {
pageIds.push( ids[ i + page * 100 ] );
}

var jsonp = document.createElement( 'script' );
jsonp.src = restApi + 'wp/v2/media/?_fields=id,media_details&_jsonp=wpPerfLab.webpUploadsFallbackWebpImages&per_page=100&include=' + pageIds.join( ',' );
document.body.appendChild( jsonp );
}
};

try {
// Loop through already available images.
loadMediaDetails( document.querySelectorAll( 'img' ) );

// Start the mutation observer to update images added dynamically.
var observer = new MutationObserver( function( mutationList ) {
for ( var i = 0; i < mutationList.length; i++ ) {
loadMediaDetails( mutationList[i].addedNodes );
}
} );

observer.observe( document.body, {
subtree: true,
childList: true,
} );
} catch ( e ) {
}
} )( document );
64 changes: 60 additions & 4 deletions modules/images/webp-uploads/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -482,12 +482,13 @@ function webp_uploads_update_image_references( $content ) {
*
* @since 1.0.0
*
* @param string $image An <img> tag where the urls would be updated.
* @param string $context The context where this is function is being used.
* @param int $attachment_id The ID of the attachment being modified.
* @param string $original_image An <img> tag where the urls would be updated.
* @param string $context The context where this is function is being used.
* @param int $attachment_id The ID of the attachment being modified.
* @return string The updated img tag.
*/
function webp_uploads_img_tag_update_mime_type( $image, $context, $attachment_id ) {
function webp_uploads_img_tag_update_mime_type( $original_image, $context, $attachment_id ) {
$image = $original_image;
$metadata = wp_get_attachment_metadata( $attachment_id );
if ( empty( $metadata['file'] ) ) {
return $image;
Expand Down Expand Up @@ -621,6 +622,16 @@ function webp_uploads_img_tag_update_mime_type( $image, $context, $attachment_id
}
}

if (
! has_action( 'wp_footer', 'webp_uploads_wepb_fallback' ) &&
$image !== $original_image &&
'the_content' === $context &&
'image/jpeg' === $original_mime &&
'image/webp' === $target_mime
) {
add_action( 'wp_footer', 'webp_uploads_wepb_fallback' );
}

return $image;
}

Expand All @@ -640,6 +651,51 @@ function webp_uploads_update_featured_image( $html, $post_id, $attachment_id ) {
}
add_filter( 'post_thumbnail_html', 'webp_uploads_update_featured_image', 10, 3 );

/**
* Adds a fallback mechanism to replace webp images with jpeg alternatives on older browsers.
*
* @since n.e.x.t
*/
function webp_uploads_wepb_fallback() {
// Get mime type transofrms for the site.
$transforms = webp_uploads_get_upload_image_mime_transforms();

// We need to add fallback only if jpeg alternatives for the webp images are enabled for the server.
$preserve_jpegs_for_jpeg_transforms = isset( $transforms['image/jpeg'] ) && in_array( 'image/jpeg', $transforms['image/jpeg'], true ) && in_array( 'image/webp', $transforms['image/jpeg'], true );
$preserve_jpegs_for_webp_transforms = isset( $transforms['image/webp'] ) && in_array( 'image/jpeg', $transforms['image/webp'], true ) && in_array( 'image/webp', $transforms['image/webp'], true );
if ( ! $preserve_jpegs_for_jpeg_transforms && ! $preserve_jpegs_for_webp_transforms ) {
return;
}

ob_start();

?>
( function( d, i, s, p ) {
s = d.createElement( s );
s.src = '<?php echo esc_url_raw( plugins_url( '/fallback.js', __FILE__ ) ); ?>';

i = d.createElement( i );
i.src = p + 'jIAAABXRUJQVlA4ICYAAACyAgCdASoCAAEALmk0mk0iIiIiIgBoSygABc6zbAAA/v56QAAAAA==';
i.onload = function() {
i.src = p + 'h4AAABXRUJQVlA4TBEAAAAvAQAAAAfQ//73v/+BiOh/AAA=';
};

i.onerror = function() {
d.body.appendChild( s );
};
} )( document, 'img', 'script', 'data:image/webp;base64,UklGR' );
<?php
$javascript = ob_get_clean();

wp_print_inline_script_tag(
preg_replace( '/\s+/', '', $javascript ),
array(
'id' => 'webpUploadsFallbackWebpImages',
'data-rest-api' => esc_url_raw( trailingslashit( get_rest_url() ) ),
)
);
}

/**
* Returns an array of image size names that have secondary mime type output enabled. Core sizes and
* core theme sizes are enabled by default.
Expand Down
38 changes: 38 additions & 0 deletions tests/modules/images/webp-uploads/load-tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,44 @@ function() {
$this->assertNotSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
}

/**
* Tests that the fallback script is added when a post with updated images is rendered.
*
* @test
*/
public function it_should_add_fallback_script_if_content_has_updated_images() {
$attachment_id = $this->factory->attachment->create_upload_object(
TESTS_PLUGIN_DIR . '/tests/testdata/modules/images/leafs.jpg'
);

apply_filters(
'the_content',
sprintf(
'<p>before image</p>%s<p>after image</p>',
wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => "wp-image-{$attachment_id}" ) )
)
);

$this->assertTrue( has_action( 'wp_footer', 'webp_uploads_wepb_fallback' ) === 10 );

$footer = get_echo( 'wp_footer' );
$this->assertStringContainsString( 'data:image/webp;base64,UklGR', $footer );
}

/**
* Tests that the fallback script is not added when a post with no updated images is rendered.
*
* @test
*/
public function it_should_not_add_fallback_script_if_content_has_no_updated_images() {
apply_filters( 'the_content', '<p>no image</p>' );

$this->assertFalse( has_action( 'wp_footer', 'webp_uploads_wepb_fallback' ) );

$footer = get_echo( 'wp_footer' );
$this->assertStringNotContainsString( 'data:image/webp;base64,UklGR', $footer );
}

/**
* Tests whether additional mime types generated only for allowed image sizes or not when the filter is used.
*
Expand Down