A lot of WordPress users assume, rightly, that images they've uploaded are compressed and optimized for the web.
This assumption is mostly true, but sometimes poorly-compressed or oversized images will slip through. We can't expect every website user to know the intricasies of image compression, and it's not unreasonable to expect that WordPress should make all uploaded images work correctly.
So let's do that. Here's how to make sure every image uploaded to WordPress is optimized.
Big image support in WordPress 5.3 added the big_image_size_threshold
filter for setting maximum image size. If an image's height or width exceeds that number (default 2560px
), WordPress scales the image down to fit, then returns that scaled copy as the largest available size. In image metadata, the new, scaled image is referenced as file
while the original, unmodified image is stored in a new original_image
key.
We can build on this new structure to create optimized copies whiie preserving original images.
Hooking into wp_generate_attachment_metadata
, we generate a new image named <filename>-optimized.jpg
. This optimized image will replace file
and the original image wil be stored as original_image
. This is basically what the threshold-handling code from wp-admin/includes/image.php does, except we address filesize instead of dimensions.
Because our filter runs after big_image_size_threshold
we can check to see if original_image
exists. If it does, there's no need for the optimized image since the scaled copy WordPress created has already been recompressed.
We also compare the filesize of the optimized image against the original to be sure the optimization was worthwhile. If the new file is less than 75% of the original, then we update the image metadata to use the new image. If the filesizes are close, then the source image was likely already optimized so we delete the new image and keep using the original.
Here's the code for making this work.
add_filter(
'wp_generate_attachment_metadata',
function ($metadata, $attachment_id) {
/**
* Check to see if 'original_image' has been created yet (`big_image_size_threshold` filter)
* If not, save out an optimized copy and update image metadata
*/
if (!array_key_exists('original_image', $metadata)) {
$uploads = wp_upload_dir();
$srcFile = $uploads['basedir'] . '/' . $metadata['file'];
$editor = wp_get_image_editor($srcFile);
if (is_wp_error($editor)) {
error_log("File $metadata[file] can not be edited.");
return $metadata;
}
/**
* WordPress does not expose the Imagick object from `wp_get_image_editor`
* so there's no way to get the compressed image's filesize before it's written
* to disk.
*/
$saved = $editor->save($editor->generate_filename('optimized'));
if (is_wp_error($saved)) {
error_log('Error trying to save.', $saved->get_error_message());
} else {
/**
* Compare filesize of the optimized image against the original
* If the optimized filesize is less than 75% of the original, then
* use the use the optimized image. If not, remove the optimized
* iamge and keep using the original image.
*/
if (filesize($saved['path']) / filesize($srcFile) < 0.75) {
// Optimization successful, update $metadata to use optimized image
// Ref: https://developer.wordpress.org/reference/functions/_wp_image_meta_replace_original/
update_attached_file($attachment_id, $saved['path']);
$metadata['original_image'] = basename($metadata['file']);
$metadata['file'] = dirname($metadata['file']) . '/' . $saved['file'];
} else {
// Optimization not worth it, delete optimized file and use original
unlink($saved['path']);
}
}
}
return $metadata;
},
10,
2
);
The JPEG compression libraries used by ImageMagick have gotten very good and the image quality of recompressed images has not been a concern. But WordPress does provide a way of setting JPEG compression quality with the jpeg_quality
hook. The default value is 82. And sometimes 90.
WordPress unfortunately marks the Image Editor's Imagick instance as protected, so there's no way to determine the size of the compressed file before writing it to disk. Disks are slow, and it would have been nice to compare filesizes before writing to disk.
Returning an image's dimensions to the big_image_size_threshold
filter wasn't enough to trick WordPress into creating scaled copies for every image. This didn't work because the logic in wp-admin/includes/image.php
is a strict less-than, not less-than-equal-to. That led to an even worse idea of trying to trick WordPress by scaling images down by 1px.
The file
key in an image's metadata contains a path fragment where images are stored in wp-uploads. The original_image
key and all stored sizes do not, they're simply the image basename. So to make use of original_image
directly, we need to reassemble the path from the dirname of file
and the value of original_image
. But at least this is quite low-level and the built-in functions take care of this.
A refined version of this code can be found in ideasonpurpose/wp-theme-init