Skip to content

Instantly share code, notes, and snippets.

@joemaller
Last active June 23, 2023 17:50
Show Gist options
  • Save joemaller/6ce1f8fd33357108ee6bab0d484dea50 to your computer and use it in GitHub Desktop.
Save joemaller/6ce1f8fd33357108ee6bab0d484dea50 to your computer and use it in GitHub Desktop.
How to make sure all images uploaded to WordPress are optimized

Compressing All Uploaded Images with WordPress

A Not Unreasonable Assumption

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.

Making it work

Building on a new feature

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.

The basics

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.

A few improvements

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.

The Code

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
);

Notes

Compression quality

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.

Efficient filesize comparisons

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.

Attempting to trick the big_image_size_threshold filter

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.

WordPress quirks

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.

@joemaller
Copy link
Author

A refined version of this code can be found in ideasonpurpose/wp-theme-init

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment