Skip to content

Commit

Permalink
Merge pull request #1772 from b1ink0/update/filter-used-for-replacing…
Browse files Browse the repository at this point in the history
…-images

Switch to wp_content_img_tag filter for Improved Image Handling
  • Loading branch information
felixarntz authored Jan 6, 2025
2 parents 597dfc6 + 8aa6f89 commit 268ce65
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 106 deletions.
76 changes: 11 additions & 65 deletions plugins/webp-uploads/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,74 +510,24 @@ function webp_uploads_remove_sources_files( int $attachment_id ): void {
add_action( 'delete_attachment', 'webp_uploads_remove_sources_files', 10, 1 );

/**
* Filters `the_content` to update images so that they use the preferred MIME type where possible.
* Filters `wp_content_img_tag` to update images so that they use the preferred MIME type where possible.
*
* By default, this is `image/webp`, if the current attachment contains the targeted MIME
* type. In the near future this will be filterable.
* @since n.e.x.t
*
* Note that most of this function will not be needed for an eventual core implementation as it
* would rely on `wp_filter_content_tags()`.
*
* @since 1.0.0
*
* @see wp_filter_content_tags()
*
* @param string|mixed $content The content of the current post.
* @return string The content with the updated references to the images.
* @param string $filtered_image Full img tag with attributes that will replace the source img tag.
* @param string $context Additional context, like the current filter name or the function name from where this was called.
* @param int $attachment_id The image attachment ID. May be 0 in case the image is not an attachment.
* @return string The updated IMG tag with references to the new MIME type if available.
*/
function webp_uploads_update_image_references( $content ): string {
if ( ! is_string( $content ) ) {
$content = '';
}

function webp_uploads_filter_image_tag( string $filtered_image, string $context, int $attachment_id ): string {
// Bail early if request is not for the frontend.
if ( ! webp_uploads_in_frontend_body() ) {
return $content;
}

// This content does not have any tag on it, move forward.
// TODO: Eventually this should use the HTML API to parse out the image tags and then update them.
if ( 0 === (int) preg_match_all( '/<(img)\s[^>]+>/', $content, $img_tags, PREG_SET_ORDER ) ) {
return $content;
}

$images = array();
foreach ( $img_tags as list( $img ) ) {
$processor = new WP_HTML_Tag_Processor( $img );
if ( ! $processor->next_tag( array( 'tag_name' => 'IMG' ) ) ) {
// This condition won't ever be met since we're iterating over the IMG tags extracted with preg_match_all() above.
continue;
}

// Find the ID of each image by the class.
// TODO: It would be preferable to use the $processor->class_list() method but there seems to be some typing issues with PHPStan.
$class_name = $processor->get_attribute( 'class' );
if (
! is_string( $class_name )
||
1 !== preg_match( '/(?:^|\s)wp-image-([1-9]\d*)(?:\s|$)/i', $class_name, $matches )
) {
continue;
}

// Make sure we use the last item on the list of matches.
$images[ $img ] = (int) $matches[1];
return $filtered_image;
}

$attachment_ids = array_unique( array_filter( array_values( $images ) ) );
if ( count( $attachment_ids ) > 1 ) {
/**
* Warm the object cache with post and meta information for all found
* images to avoid making individual database calls.
*/
_prime_post_caches( $attachment_ids, false, true );
}
$filtered_image = str_replace( $filtered_image, webp_uploads_img_tag_update_mime_type( $filtered_image, 'the_content', $attachment_id ), $filtered_image );

foreach ( $images as $img => $attachment_id ) {
$content = str_replace( $img, webp_uploads_img_tag_update_mime_type( $img, 'the_content', $attachment_id ), $content );
}

return $content;
return $filtered_image;
}

/**
Expand Down Expand Up @@ -773,11 +723,7 @@ function webp_uploads_render_generator(): void {
* @since 2.1.0
*/
function webp_uploads_init(): void {
if ( webp_uploads_is_picture_element_enabled() ) {
add_filter( 'wp_content_img_tag', 'webp_uploads_wrap_image_in_picture', 10, 3 );
} else {
add_filter( 'the_content', 'webp_uploads_update_image_references', 13 ); // Run after wp_filter_content_tags.
}
add_filter( 'wp_content_img_tag', webp_uploads_is_picture_element_enabled() ? 'webp_uploads_wrap_image_in_picture' : 'webp_uploads_filter_image_tag', 10, 3 );
}
add_action( 'init', 'webp_uploads_init' );

Expand Down
2 changes: 1 addition & 1 deletion plugins/webp-uploads/tests/data/class-testcase.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public function set_image_output_type( string $format ): void {
*/
public function opt_in_to_picture_element(): void {
remove_filter( 'wp_content_img_tag', 'webp_uploads_wrap_image_in_picture', 10 );
remove_filter( 'the_content', 'webp_uploads_update_image_references', 13 ); // Run after wp_filter_content_tags.
remove_filter( 'wp_content_img_tag', 'webp_uploads_filter_image_tag', 10 );

// Apply picture element support.
update_option( 'webp_uploads_use_picture_element', '1' );
Expand Down
74 changes: 37 additions & 37 deletions plugins/webp-uploads/tests/test-load.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,38 +441,38 @@ public function test_it_should_remove_the_backup_sizes_and_sources_if_the_attach
/**
* Avoid the change of URLs of images that are not part of the media library
*
* @covers ::webp_uploads_update_image_references
* @group webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_avoid_the_change_of_urls_of_images_that_are_not_part_of_the_media_library(): void {
// Run critical hooks to satisfy webp_uploads_in_frontend_body() conditions.
$this->mock_frontend_body_hooks();

$paragraph = '<p>Donec accumsan, sapien et <img src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">, id commodo nisi sapien et est. Mauris nisl odio, iaculis vitae pellentesque nec.</p>';
$paragraph = '<img src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">';

$this->assertSame( $paragraph, webp_uploads_update_image_references( $paragraph ) );
$this->assertSame( $paragraph, webp_uploads_filter_image_tag( $paragraph, 'the_content', 0 ) );
}

/**
* Avoid replacing not existing attachment IDs
*
* @covers ::webp_uploads_update_image_references
* @group webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_avoid_replacing_not_existing_attachment_i_ds(): void {
// Run critical hooks to satisfy webp_uploads_in_frontend_body() conditions.
$this->mock_frontend_body_hooks();

$paragraph = '<p>Donec accumsan, sapien et <img class="wp-image-0" src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">, id commodo nisi sapien et est. Mauris nisl odio, iaculis vitae pellentesque nec.</p>';
$paragraph = '<img class="wp-image-0" src="https://ia600200.us.archive.org/16/items/SPD-SLRSY-1867/hubblesite_2001_06.jpg">';

$this->assertSame( $paragraph, webp_uploads_update_image_references( $paragraph ) );
$this->assertSame( $paragraph, webp_uploads_filter_image_tag( $paragraph, 'the_content', 0 ) );
}

/**
* Prevent replacing a WebP image
*
* @covers ::webp_uploads_update_image_references
* @group webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_prevent_replacing_a_webp_image(): void {
// Create JPEG and WebP to check that WebP does not get replaced with JPEG.
Expand All @@ -495,13 +495,13 @@ public function test_it_should_prevent_replacing_a_webp_image(): void {
$this->assertNotSame( $tag, $expected_tag );
$this->assertSame( $expected_tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertSame( $expected_tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $expected_tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* Prevent replacing a jpg image if the image does not have the target class name
*
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
*/
public function test_it_should_prevent_replacing_a_jpg_image_if_the_image_does_not_have_the_target_class_name(): void {
$attachment_id = self::factory()->attachment->create_upload_object(
Expand All @@ -513,16 +513,16 @@ public function test_it_should_prevent_replacing_a_jpg_image_if_the_image_does_n

$tag = wp_get_attachment_image( $attachment_id, 'medium' );

$this->assertSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* Replace references to a JPG image to a WebP version
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @dataProvider provider_replace_images_with_different_extensions
* @group webp_uploads_update_image_references
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_replace_references_to_a_jpg_image_to_a_webp_version( string $image_path ): void {
// Create JPEG and WebP to check replacement of JPEG => WebP.
Expand All @@ -543,16 +543,16 @@ public function test_it_should_replace_references_to_a_jpg_image_to_a_webp_versi
$this->assertNotSame( $tag, $expected_tag );
$this->assertSame( $expected_tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertSame( $expected_tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $expected_tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* Should not replace jpeg images in the content if other mime types are disabled via filter.
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @dataProvider provider_replace_images_with_different_extensions
* @group webp_uploads_update_image_references
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_not_replace_the_references_to_a_jpg_image_when_disabled_via_filter( string $image_path ): void {
remove_all_filters( 'webp_uploads_content_image_mimes' );
Expand All @@ -570,7 +570,7 @@ static function ( $mime_types ) {

$this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

public function provider_replace_images_with_different_extensions(): Generator {
Expand All @@ -582,7 +582,7 @@ public function provider_replace_images_with_different_extensions(): Generator {
* Replace all the images including the full size image
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
*/
public function test_it_should_replace_all_the_images_including_the_full_size_image(): void {
// Create JPEG and WebP to check replacement of JPEG => WebP.
Expand All @@ -602,17 +602,17 @@ public function test_it_should_replace_all_the_images_including_the_full_size_im
$this->assertSame( $expected, wp_check_filetype( get_attached_file( $attachment_id ) ) );
$this->mock_frontend_body_hooks();
$this->assertStringNotContainsString( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->assertStringNotContainsString( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_update_image_references( $tag ) );
$this->assertStringNotContainsString( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
$this->assertStringContainsString( $metadata['sources']['image/webp']['file'], webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->assertStringNotContainsString( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_update_image_references( $tag ) );
$this->assertStringNotContainsString( wp_basename( get_attached_file( $attachment_id ) ), webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* Prevent replacing an image with no available sources
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @group webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_prevent_replacing_an_image_with_no_available_sources(): void {
add_filter( 'webp_uploads_upload_image_mime_transforms', '__return_empty_array' );
Expand All @@ -622,16 +622,16 @@ public function test_it_should_prevent_replacing_an_image_with_no_available_sour
$tag = wp_get_attachment_image( $attachment_id, 'full', false, array( 'class' => "wp-image-{$attachment_id}" ) );
$this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* Prevent update not supported images with no available sources
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @dataProvider data_provider_not_supported_webp_images
* @group webp_uploads_update_image_references
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_prevent_update_not_supported_images_with_no_available_sources( string $image_path ): void {
$attachment_id = self::factory()->attachment->create_upload_object( $image_path );
Expand All @@ -641,7 +641,7 @@ public function test_it_should_prevent_update_not_supported_images_with_no_avail

$this->assertSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

public function data_provider_not_supported_webp_images(): Generator {
Expand Down Expand Up @@ -800,8 +800,8 @@ public function data_provider_supported_image_types_with_threshold(): array {
/**
* Prevent replacing an image if image was uploaded via external source or plugin.
*
* @covers ::webp_uploads_update_image_references
* @group webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
* @group webp_uploads_filter_image_tag
*/
public function test_it_should_prevent_replacing_an_image_uploaded_via_external_source(): void {
remove_all_filters( 'webp_uploads_pre_replace_additional_image_source' );
Expand All @@ -818,14 +818,14 @@ static function () {
$tag = wp_get_attachment_image( $attachment_id, 'medium', false, array( 'class' => "wp-image-{$attachment_id}" ) );
$this->assertNotSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertNotSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertNotSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* The image with the smaller filesize should be used when webp_uploads_discard_larger_generated_images is set to true.
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
*/
public function test_it_should_create_webp_when_webp_is_smaller_than_jpegs(): void {
// Create JPEG and WebP.
Expand All @@ -844,7 +844,7 @@ public function test_it_should_create_webp_when_webp_is_smaller_than_jpegs(): vo
$this->assertImageHasSource( $attachment_id, 'image/webp' );
$this->assertImageHasSizeSource( $attachment_id, 'thumbnail', 'image/webp' );
$this->mock_frontend_body_hooks();
$this->assertSame( $result, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $result, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );

$this->assertNotSame( $tag, $result );

Expand All @@ -867,7 +867,7 @@ public function test_it_should_create_webp_when_webp_is_smaller_than_jpegs(): vo
* The image with the smaller filesize should be used when webp_uploads_discard_larger_generated_images is set to true.
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
*/
public function test_it_should_create_webp_for_full_size_which_is_smaller_in_webp_format(): void {
// Create JPEG and WebP.
Expand All @@ -890,14 +890,14 @@ public function test_it_should_create_webp_for_full_size_which_is_smaller_in_web
}
$this->assertNotSame( $tag, webp_uploads_img_tag_update_mime_type( $tag, 'the_content', $attachment_id ) );
$this->mock_frontend_body_hooks();
$this->assertNotSame( $tag, webp_uploads_update_image_references( $tag ) );
$this->assertNotSame( $tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
* The image with the smaller filesize should be used when webp_uploads_discard_larger_generated_images is set to true.
*
* @covers ::webp_uploads_img_tag_update_mime_type
* @covers ::webp_uploads_update_image_references
* @covers ::webp_uploads_filter_image_tag
*/
public function test_it_should_create_webp_for_some_sizes_which_are_smaller_in_webp_format(): void {
// Create JPEG and WebP.
Expand Down Expand Up @@ -926,7 +926,7 @@ public function test_it_should_create_webp_for_some_sizes_which_are_smaller_in_w
$this->assertNotSame( $tag, $updated_tag );
$this->assertSame( $expected_tag, $updated_tag );
$this->mock_frontend_body_hooks();
$this->assertSame( $expected_tag, webp_uploads_update_image_references( $tag ) );
$this->assertSame( $expected_tag, webp_uploads_filter_image_tag( $tag, 'the_content', $attachment_id ) );
}

/**
Expand Down
7 changes: 4 additions & 3 deletions plugins/webp-uploads/tests/test-picture-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ public function test_maybe_wrap_images_in_picture_element( bool $fallback_jpeg,

$expected_html = str_replace( array_keys( $replacements ), array_values( $replacements ), $expected_html );

// Apply the wp_content_img_tag filter.
$image = apply_filters( 'wp_content_img_tag', $image, 'the_content', self::$image_id );

if ( webp_uploads_is_picture_element_enabled() ) {
// Apply the wp_content_img_tag filter.
$image = apply_filters( 'wp_content_img_tag', $image, 'the_content', self::$image_id );
}
// Check that the image has the expected HTML.
$this->assertEquals( $expected_html, $image );
}
Expand Down

0 comments on commit 268ce65

Please sign in to comment.