Skip to content

Commit

Permalink
Merge pull request #418 from WordPress/fix/372-discard-larger-webp
Browse files Browse the repository at this point in the history
Discard WebP image if it is larger than corresponding JPEG image
  • Loading branch information
felixarntz committed Jul 14, 2022
2 parents 3171285 + 5a6694c commit 6164fbe
Show file tree
Hide file tree
Showing 6 changed files with 271 additions and 118 deletions.
32 changes: 32 additions & 0 deletions modules/images/webp-uploads/helper.php
Expand Up @@ -240,3 +240,35 @@ function webp_uploads_get_attachment_sources( $attachment_id, $size = 'thumbnail
// Return an empty array if no sources found.
return array();
}

/**
* Check whether the additional image is larger than the original image.
*
* @since n.e.x.t
*
* @param array $original An array with the metadata of the attachment.
* @param array $additional An array containing the filename and file size for additional mime.
* @return bool True if the additional image is larger than the original image, otherwise false.
*/
function webp_uploads_should_discard_additional_image_file( array $original, array $additional ) {
$original_image_filesize = isset( $original['filesize'] ) ? (int) $original['filesize'] : 0;
$additional_image_filesize = isset( $additional['filesize'] ) ? (int) $additional['filesize'] : 0;
if ( $original_image_filesize > 0 && $additional_image_filesize > 0 ) {
/**
* Filter whether WebP images that are larger than the matching JPEG should be discarded.
*
* By default the performance lab plugin will use the mime type with the smaller filesize
* rather than defaulting to `webp`.
*
* @since n.e.x.t
*
* @param bool $preferred_filesize Prioritize file size over mime type. Default true.
*/
$webp_discard_larger_images = apply_filters( 'webp_uploads_discard_larger_generated_images', true );

if ( $webp_discard_larger_images && $additional_image_filesize >= $original_image_filesize ) {
return true;
}
}
return false;
}
172 changes: 77 additions & 95 deletions modules/images/webp-uploads/load.php
Expand Up @@ -96,6 +96,11 @@ function webp_uploads_create_sources_property( array $metadata, $attachment_id )
continue;
}

if ( webp_uploads_should_discard_additional_image_file( $metadata, $image ) ) {
wp_delete_file_from_directory( $destination, $original_directory );
continue;
}

$metadata['sources'][ $targeted_mime ] = $image;
wp_update_attachment_metadata( $attachment_id, $metadata );
}
Expand Down Expand Up @@ -156,6 +161,12 @@ function webp_uploads_create_sources_property( array $metadata, $attachment_id )
continue;
}

if ( webp_uploads_should_discard_additional_image_file( $properties, $source ) ) {
$destination = path_join( $original_directory, $source['file'] );
wp_delete_file_from_directory( $destination, $original_directory );
continue;
}

$properties['sources'][ $mime ] = $source;
$metadata['sizes'][ $size_name ] = $properties;
wp_update_attachment_metadata( $attachment_id, $metadata );
Expand Down Expand Up @@ -481,6 +492,7 @@ function webp_uploads_update_image_references( $content ) {
* for the specified image sizes, the *.webp references are stored inside of each size.
*
* @since 1.0.0
* @since n.e.x.t Remove `webp_uploads_prefer_smaller_image_file` filter
*
* @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.
Expand All @@ -490,24 +502,11 @@ function webp_uploads_update_image_references( $content ) {
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;
}

/**
* Filters whether the smaller image should be used regardless of which MIME type is preferred overall.
*
* This is disabled by default only because it is not part of the current WordPress core feature proposal.
*
* By enabling this, the plugin will compare the image file sizes and prefer the smaller file regardless of MIME
* type.
*
* @since 1.0.0
*
* @param bool $prefer_smaller_image_file Whether to prefer the smaller image file.
*/
$prefer_smaller_image_file = apply_filters( 'webp_uploads_prefer_smaller_image_file', false );

/**
* Filters mime types that should be used to update all images in the content. The order of
* mime types matters. The first mime type in the list will be used if it is supported by an image.
Expand All @@ -523,58 +522,73 @@ function webp_uploads_img_tag_update_mime_type( $original_image, $context, $atta
// Get the original mime type for comparison.
$original_mime = get_post_mime_type( $attachment_id );

$target_mime = null;
foreach ( $target_mimes as $mime ) {
if ( isset( $metadata['sources'][ $mime ] ) ) {
$target_mime = $mime;
break;
foreach ( $target_mimes as $target_mime ) {

if ( $target_mime === $original_mime ) {
continue;
}
}

if ( null === $target_mime ) {
return $image;
}
if ( ! isset( $metadata['sources'][ $target_mime ]['file'] ) ) {
continue;
}

// Replace the full size image if present.
if ( isset( $metadata['sources'][ $target_mime ]['file'] ) ) {
// Initially set the target mime as the replacement source.
$replacement_source = $metadata['sources'][ $target_mime ]['file'];
/**
* Filter to replace additional image source file, by locating the original
* mime types of the file and return correct file path in the end.
*
* Altering the $image tag through this filter effectively short-circuits the default replacement logic using the preferred MIME type.
*
* @since 1.1.0
*
* @param string $image An <img> tag where the urls would be updated.
* @param int $attachment_id The ID of the attachment being modified.
* @param string $size The size name that would be used to create this image, out of the registered subsizes.
* @param string $target_mime The target mime in which the image should be created.
* @param string $context The context where this is function is being used.
*/
$filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, 'full', $target_mime, $context );

// Check for the smaller image file.
if (
$prefer_smaller_image_file &&
! empty( $metadata['sources'][ $target_mime ]['filesize'] ) &&
! empty( $metadata['sources'][ $original_mime ]['filesize'] ) &&
$metadata['sources'][ $original_mime ]['filesize'] < $metadata['sources'][ $target_mime ]['filesize']
) {
// Set the original source file as the replacement if smaller.
$replacement_source = $metadata['sources'][ $original_mime ]['file'];
// If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
if ( $filtered_image === $image ) {
$basename = wp_basename( $metadata['file'] );
$image = str_replace(
$basename,
$metadata['sources'][ $target_mime ]['file'],
$image
);
} else {
$image = $filtered_image;
}
}

// Replace sub sizes for the image if present.
foreach ( $metadata['sizes'] as $size => $size_data ) {
if ( empty( $size_data['file'] ) ) {
continue;
}

$basename = wp_basename( $metadata['file'] );
if ( $basename !== $replacement_source ) {
foreach ( $target_mimes as $target_mime ) {

/**
* Filter to replace additional image source file, by locating the original
* mime types of the file and return correct file path in the end.
*
* Altering the $image tag through this filter effectively short-circuits the default replacement logic using the preferred MIME type.
*
* @since 1.1.0
*
* @param string $image An <img> tag where the urls would be updated.
* @param int $attachment_id The ID of the attachment being modified.
* @param string $size The size name that would be used to create this image, out of the registered subsizes.
* @param string $target_mime The target mime in which the image should be created.
* @param string $context The context where this is function is being used.
*/
$filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, 'full', $target_mime, $context );
if ( $target_mime === $original_mime ) {
continue;
}

if ( ! isset( $size_data['sources'][ $target_mime ]['file'] ) ) {
continue;
}

if ( $size_data['file'] === $size_data['sources'][ $target_mime ]['file'] ) {
continue;
}

/** This filter is documented in modules/images/webp-uploads/load.php */
$filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, $size, $target_mime, $context );

// If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
if ( $filtered_image === $image ) {
$image = str_replace(
$basename,
$metadata['sources'][ $target_mime ]['file'],
$size_data['file'],
$size_data['sources'][ $target_mime ]['file'],
$image
);
} else {
Expand All @@ -583,53 +597,21 @@ function webp_uploads_img_tag_update_mime_type( $original_image, $context, $atta
}
}

// Replace sub sizes for the image if present.
foreach ( $metadata['sizes'] as $size => $size_data ) {
if ( empty( $size_data['file'] ) ) {
continue;
}

if ( empty( $size_data['sources'][ $target_mime ]['file'] ) ) {
continue;
}
foreach ( $target_mimes as $target_mime ) {

if ( $size_data['file'] === $size_data['sources'][ $target_mime ]['file'] ) {
if ( $target_mime === $original_mime ) {
continue;
}

// Do not update image URL if the target image is larger than the original.
if (
$prefer_smaller_image_file &&
! empty( $size_data['sources'][ $target_mime ]['filesize'] ) &&
! empty( $size_data['sources'][ $original_mime ]['filesize'] ) &&
$size_data['sources'][ $original_mime ]['filesize'] < $size_data['sources'][ $target_mime ]['filesize']
! has_action( 'wp_footer', 'webp_uploads_wepb_fallback' ) &&
$image !== $original_image &&
'the_content' === $context &&
'image/jpeg' === $original_mime &&
'image/webp' === $target_mime
) {
continue;
add_action( 'wp_footer', 'webp_uploads_wepb_fallback' );
}

/** This filter is documented in modules/images/webp-uploads/load.php */
$filtered_image = (string) apply_filters( 'webp_uploads_pre_replace_additional_image_source', $image, $attachment_id, $size, $target_mime, $context );

// If filtered image is same as the image, run our own replacement logic, otherwise rely on the filtered image.
if ( $filtered_image === $image ) {
$image = str_replace(
$size_data['file'],
$size_data['sources'][ $target_mime ]['file'],
$image
);
} else {
$image = $filtered_image;
}
}

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 Down
44 changes: 44 additions & 0 deletions tests/modules/images/webp-uploads/helper-tests.php
Expand Up @@ -410,4 +410,48 @@ function () {
$this->assertIsArray( $transforms );
$this->assertSame( array( 'image/jpeg' => array( 'image/jpeg', 'image/webp' ) ), $transforms );
}

/**
* @dataProvider data_provider_image_filesize
*
* @test
*/
public function it_should_discard_additional_image_if_larger_than_the_original_image( $original_filesize, $additional_filesize, $expected_status ) {
add_filter( 'webp_uploads_discard_larger_generated_images', '__return_true' );

$output = webp_uploads_should_discard_additional_image_file( $original_filesize, $additional_filesize );
$this->assertSame( $output, $expected_status );
}

public function data_provider_image_filesize() {
return array(
array(
array( 'filesize' => 120101 ),
array( 'filesize' => 100101 ),
false,
),
array(
array( 'filesize' => 100101 ),
array( 'filesize' => 120101 ),
true,
),
array(
array( 'filesize' => 10101 ),
array( 'filesize' => 10101 ),
true,
),
);
}

/**
* @dataProvider data_provider_image_filesize
*
* @test
*/
public function it_should_never_discard_additional_image_if_filter_is_false( $original_filesize, $additional_filesize ) {
add_filter( 'webp_uploads_discard_larger_generated_images', '__return_false' );

$output = webp_uploads_should_discard_additional_image_file( $original_filesize, $additional_filesize );
$this->assertFalse( $output );
}
}
6 changes: 6 additions & 0 deletions tests/modules/images/webp-uploads/image-edit-tests.php
Expand Up @@ -10,6 +10,12 @@

class WebP_Uploads_Image_Edit_Tests extends ImagesTestCase {

public function set_up() {
parent::set_up();

add_filter( 'webp_uploads_discard_larger_generated_images', '__return_false' );
}

/**
* Backup the sources structure alongside the full size
*
Expand Down

0 comments on commit 6164fbe

Please sign in to comment.