Changing JPEG compression depending on image size

A very special filter

The jpeg_quality filter is a really special one: It gets used in three different cases and you’ve to use the second argument to determine, if you want to use the filter or not.

Don’t let it do everything

The main problem for such a special filter is, that it may fire for later actions, if you ain’t remove it – allow it to run after the first check. So we need to get another filter inside wp_save_image_file() to check if we want to alter the compression or not. To disable it for another save process, we remove it right before altering the compression.

The kool kid

The really odd thing is, that WP uses a default compression of 90% (which is 10% reduced quality) for every save process. This means, that every time you upload/crop/edit an image, you reduce its quality… which is a pain for images that aren’t the best, when you upload them (test it with an image containing a lot of red with a high contrast background). But… The really neat thing is, that you can change this behavior. So you want to change the compression, but get increased quality – much better than core allows- at the same time.

/**
 * Alter the image compression, depending on case
 * 
 * @param  int $compression
 * @param  string $case
 * @return int $compression
 */
function wpse58600_custom_jpg_compression_cb( $compression, $case )
{
    global $size_switch;

    // Should only fire once - don't leave it in for later cases
    remove_filter( current_filter(), __FUNCTION__ );

    // Alter the compression, depending on the case
    switch ( $case )
    {
        case( 'edit_image' ) :
            // We only add the compression, if the switch triggered,
            // which means, that the size is smaller, than set in the main function.
            // 60 is the percentage value, that gets added as compression to the smaller images.
            $compression = $size_switch ? 60 : 100;
            break;

        case( 'image_resize' ) :
            // Better leave it on 100% for resize
            $compression = 100;
            break;

        case( 'wp_crop_image' ) :
            // Better leave it on 100% for crop
            // We already compressed it on the camera, the desktop/handheld device 
            // and the server previously. That's enough so far.
            $compression = 100;
            break;
    }

    return $compression;
}

/**
 * Alter the compression for JPEG mime type images
 * Checks for a specific min size of the image, before altering it
 * 
 * @param  string $image
 * @param  int $post_id 
 * @return string $image
 */
function wpse58600_custom_jpg_compression( $image, $post_id )
{
    global $size_switch;
    $size_switch = false;

    // Define the size, that stops adding a compression
    $trigger_size = 641;

    // Get the sizes
    $size_x = imagesx( $image );
    $size_y = imagesy( $image );

    // Add the filter only in case
    if ( $trigger_size < $size_x )
    {
        $size_switch = true;
    }
    add_filter( 'jpeg_quality', 'wpse58600_custom_jpg_compression_cb', 20, 2 );

    return $image;
}
add_filter( 'image_save_pre', 'wpse58600_custom_jpg_compression', 20, 2 );

EDIT: After a short discussion with @toscho, he pointed out, that the whole callback could be reduced to the following:

function wpse58600_custom_jpg_compression_cb( $compression, $case )
{
    // Should only fire once - don't leave it in for later cases
    remove_filter( current_filter(), __FUNCTION__ );

    return ( $GLOBALS['size_switch'] && 'edit_image' === $case ) ? 60 : 100;
}

As I pulled the code out of a plugin I’m currently working on, I needed the switch to add settings in. I also have to note, that I don’t use the global in my plugin, as it’s an OOP approach. The code you can read ↑ above, is mainly reduced and altered code from the plugin, that has some minor left overs and is meant to be explanatory for later readers and still works. If you want to use it as plugin, you can do some optimization depending on your personal use case.


Notes:

From some investigation on the different tasks, there are, I noticed, that multiple $cases get triggered on the following steps:

  • Rotate: edit-image » image-resize (the later 1× for any any size you choose – Thumbnail, etc.)
  • Mirror: edit-image » image-resize (–“–)

This means, that the filter callback for jpeq_quality will trigger 2× for rotating/mirroring an image and +1× for any additional size you add. So if you got less than 100%, it will reduce the quality twice. I did a lot of research on this topic, but I’m still not completely sure, what exact functions cause this behavior.

Leave a Comment