2

I am working on a fairly complex redesign of a wordpress project. Currently I am working on the image implementation. The site is very image heavy and I want to provide the optimal image for each application. I thought about something like this:

1. Orientation

The image can be in landscape, portait or square format. Each has a different max-width. The optimal solution would be to provide a different version for each orientation.

Landscape
Portrait
Square

2. High PPI

I would also like to provide a @2x version of each image:

Landscape
Landscape@2x

Portrait
Portrait@2x

Square
Square@2x

3. Breakpoints

The final site comes with at least four breakpoints. I would also like to optimize the image for each breakpoint:

Breakpoint 1 | Breakpoint 2 | Breakpoint 3 | Breakpoint 4
-------------|--------------|--------------|-------------
Landscape    | Landscape    | Landscape    | Landscape   
Landscape@2x | Landscape@2x | Landscape@2x | Landscape@2x
             |              |              |
Portrait     | Portrait     | Portrait     | Portrait    
Portrait@2x  | Portrait@2x  | Portrait@2x  | Portrait@2x 
             |              |              |
Square       | Square       | Square       | Square      
Square@2x    | Square@2x    | Square@2x    | Square@2x    

The Problem

In the end, this gives me 24 generated images per image, which is a huge amount of generated data. Two thirds of that data will never be used (the two other orientations) and I would love to strip those out. add_image_size() does not take the image orientation into account.

Is there any way to only generate the images needed for the orientation of the image?


Update

TL;DR: I may have found a solution, code is at the bottom, further testing tomorrow.

Ok, I took a deep dive into the Wordpress Trac today, and I think I have got it.

It is hard to alter the generation of the add_image_size() function, so I took the wp_generate_attachment_metadata hook,

The images get generated in the wp_generate_attachment_metadata() function. It is hard to alter the function, because the matching wp_generate_attachment_metadata hook takes place, after the images have been generated.

Still, this was the best hook I have found for this purpose. I decided to create my own resized copies of the original image (WP_Image_Editor made that fairly easy) and include the newly generated images in the metadata of the original image.

So far this looks pretty good, except, that the progressbar stays on 100% until the images have been processed. This could be a problem with users leaving the page, before all images have been processed. The second problem was, that the custom generated images do not get deleted, if I delete the attachment. This is because of the get_intermediate_image_sizes() function, which looks for image sizes, added by add_image_size(). I have found the intermediate_image_sizes hook and want to try to add all possible image sizes to it tomorrow, in the meantime I have a quick solution, which makes use of the delete_attachment hook.

I will do further testing tomorrow and update this post.

Here is my code so far:

/**
 * Generates custom image sizes, depending on the image orientation. Use the wp_generate_attachment_metadata hook!
 */
function r21_create_custom_image_sizes($meta) {

    // Initialize variables
    global $r21_image_sizes;
    $image_sizes = '';
    $new_meta = array();

    // Generate the full file path for the image
    $image['path'] = path_join(wp_upload_dir()['basedir'], $meta['file']);

    // Get the dimensions of the original image
    list($image['width'], $image['height'], $image['type']) = getimagesize($image['path']);

    // Check the image orientation
    if ($image['width'] > $image['height']) {
        // Landscape
        $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'landscape', true);

    } else if ($image['width'] < $image['height']) {
        // Portrait
        $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'portrait', true);

    } else {
        // Square
        $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'square', true);
    }

    // Iterate through the sizes to be generated
    foreach ($image_sizes as $size) {
        // TODO: Check if an image in the requested dimensions allready exists.

        // Create an instance of WP_Image_Editor for the original image
        $new_image = wp_get_image_editor($image['path']);

        // Resize the image
        $new_image->resize($size['width'], $size['height'], $size['crop']);

        // Save new image and store new meta data in variable
        $new_image_meta = $new_image->save();

        // Reflect back new metadata
        $meta['sizes'][$size['name']]['file'] = $new_image_meta['file'];
        $meta['sizes'][$size['name']]['width'] = $new_image_meta['width'];
        $meta['sizes'][$size['name']]['height'] = $new_image_meta['height'];
        $meta['sizes'][$size['name']]['mime-type'] = $new_image_meta['mime-type'];
    }

    return $meta;
}
add_filter('wp_generate_attachment_metadata', 'r21_create_custom_image_sizes');



/**
 * Deletes images, generated by r21_create_custom_image_sizes(). Use the delete_attachment hook!
 */
function r21_delete_custom_image_size_files($post_id) {
    $sizes_meta = wp_get_attachment_metadata($post_id)['sizes'];

    foreach ($sizes_meta as $size) {
        // TODO: Add support for wp_delete_file hook here
        @ unlink(path_join(wp_upload_dir()['path'], $size['file']));
    }
}
add_action('delete_attachment', 'r21_delete_custom_image_size_files');
7
  • I actually can't get you - why do you need those breakpoints? If you need them, then why the 24 files per file is an issue, because you know that's the way? Why not only max-width:100%; height:auto isn't solving your problem? Commented Oct 28, 2014 at 13:09
  • Let’s say the first breakpoint is optimized for mobile devices. The image in this breakpoint has a maximum width of 300px. If the fourth breakboint is optimized for widescreen desktops and the image has a maximum width of 1500px, it would be a huge difference in file size. If I am on the mobile optimized breakpoint, there would be no need to load the 1500px image if it will be displayed with a max-width of 300px. Instead, I would request a 300px version and keep the page load small. It’s simply a matter of file-size.
    – Afterlame
    Commented Oct 28, 2014 at 15:25
  • As for the problem with the 24 images: The site I am talking about is unsing thousands of images. If I generate 24 images for each of them, it would generate a huge amount of files. Especially unneded files, because 16 of them will never be used. It takes a lot of time to process the images and I will have gigabytes of unneeded data stored on the server. This would result in unnecessary costs for hosting.
    – Afterlame
    Commented Oct 28, 2014 at 15:33
  • Do you want to do this automatically in WP, or can you use an external program and then upload the images? If you can use an external program, just determine orientation and convert to just the 4 images you need. You could also convert on the fly, but that might become CPU intensive if you get a lot of traffic.
    – user4849
    Commented Oct 28, 2014 at 16:47
  • @barrycarter I would love to do it inside Wordpress as part of the image size functions. There are a lot of benefits in not using external software, but the main parts are: It is easier to maintain and a lot of possible error sources get eliminated by not adding additional steps for the editor. I thought about converting on the fly too, but as you said, this could be very heavy on the server.
    – Afterlame
    Commented Oct 28, 2014 at 22:50

1 Answer 1

1

Here is my working solution. The code is documented, so it should be clear what each function does.

I pretty much use the wp_generate_attachment_metadata filter to create the needed images after the image has been uploaded.

The generated images are listed in the metadata too, like any other intermediate image size. That way you can pretty much work with them like with any other image size.

Another important part is to use the delete_attachment filter to delte the generated images.

// ==========================
// Custom Image Size Handling
// ==========================

/**
 * Removes default and plugin generated image sizes.
 * This is optional!
 */
function r21_remove_image_sizes($sizes) {
  unset($sizes['thumbnail']);
  unset($sizes['medium']);
  unset($sizes['large']);

  return $sizes;
}
add_filter('intermediate_image_sizes', 'r21_remove_image_sizes');
add_filter('intermediate_image_sizes_advanced', 'r21_remove_image_sizes');


/**
 * Generate a handle for thumbnail regeneration tools.
 * The custom images will always be regenerated after one of
 * the site wide image sizes have been regenerated.
 * The problem here is, that if there are no site wide image sizes
 * defined, you can not regenerate any custom size images.
 * To avoid this we create a 1x1 px image that works as handle, if there
 * are no other imge sizes.
 */
add_image_size( 'cstm-img-regeneration-handle' , 1, 1, array( 'left', 'top' ));


/**
 * Delete unneeded generated images and their metadata.
 * Also deletes images, generated in the filter, this is why
 * this function has to be used before the image generation function.
 *
 * @param array $attachment_meta
 * @return array
 */
function r21_remove_old_image_sizes($attachment_meta) {

  foreach ($attachment_meta['sizes'] as $size_name => $size_data) {
    // Ceck if image size is currently an active intermediate image size
    if (array_key_exists($size_name, get_intermediate_image_sizes())) { continue; }

    // Delete file
    @ unlink(path_join(r21_get_attachment_path_by('meta', $attachment_meta), $attachment_meta['sizes'][$size_name]['file']));

    // Delete metadata
    unset($attachment_meta['sizes'][$size_name]);
  }

  return $attachment_meta;
}
add_filter('wp_generate_attachment_metadata', 'r21_remove_old_image_sizes', 10, 1);


/**
 * Removes the the custom image regneration handle image, if existing.
 *
 * @return array Returns the metadata, without the handle image entry.
 */
function r21_remove_regeneration_hook_image($attachment_meta) {

  $name = 'cstm-img-regeneration-handle';

  // Check if image exists
  if (array_key_exists($name, $attachment_meta['sizes'])) {
    // Delete Image File
    @ unlink(path_join(r21_get_attachment_path_by('meta', $attachment_meta), $attachment_meta['sizes'][$name]['file']));

    // Delete Image Metadata
    unset($attachment_meta['sizes'][$name]);
  }

  return $attachment_meta;
}
add_filter('wp_generate_attachment_metadata', 'r21_remove_regeneration_hook_image', 10, 1);


/**
 * Generates custom image sizes, depending on the image orientation. Use the wp_generate_attachment_metadata hook!
 */
function r21_create_custom_image_sizes($meta) {

  // Initialize variables
  global $r21_image_sizes;
  $image_sizes = '';

  // Generate the full file path for the image
  $image['path'] = path_join(wp_upload_dir()['basedir'], $meta['file']);

  // Get the dimensions of the original image
  list($image['width'], $image['height'], $image['type']) = getimagesize($image['path']);

  // Check the image orientation
  if ($image['width'] > $image['height']) {
    // Landscape
    $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'landscape', true);

  } else if ($image['width'] < $image['height']) {
    // Portrait
    $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'portrait', true);

  } else {
    // Square
    $image_sizes = r21_filter_custom_image_sizes($r21_image_sizes, 'square', true);
  }

  // Iterate through the sizes to be generated
  foreach ($image_sizes as $size_name => $size) {
    // TODO: Check if an image in the requested dimensions allready exists.

    // Create an instance of WP_Image_Editor for the original image
    $new_image = wp_get_image_editor($image['path']);

    // Check if there is an error
    if (is_wp_error($new_image)) { continue; }

    // Resize the image
    $new_image->resize($size['width'], $size['height'], $size['crop']);

    // Save new image and store new meta data in variable
    $new_image_meta = $new_image->save();

    // Reflect back new metadata
    $meta['sizes'][$size_name]['file'] = $new_image_meta['file'];
    $meta['sizes'][$size_name]['width'] = $new_image_meta['width'];
    $meta['sizes'][$size_name]['height'] = $new_image_meta['height'];
    $meta['sizes'][$size_name]['mime-type'] = $new_image_meta['mime-type'];
  }

  return $meta;
}
add_filter('wp_generate_attachment_metadata', 'r21_create_custom_image_sizes', 10, 1);


/**
 * Deletes images, generated by r21_create_custom_image_sizes(). Use the delete_attachment hook!
 */
function r21_delete_custom_image_size_files($post_id) {
  $meta = wp_get_attachment_metadata($post_id);

  foreach ($meta['sizes'] as $size) {
    // TODO: Add support for wp_delete_file hook here
    @ unlink(path_join(r21_get_attachment_path_by('meta', $meta), $size['file']));
  }
}
add_action('delete_attachment', 'r21_delete_custom_image_size_files');

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.