Each custom image size in custom upload directory?

Philipp, anything is possible if you set your mind to it. You can solve your issue by extending the WordPress image editor class.

Note I’m using WordPress 3.7 – I haven’t checked any of the below code in earlier versions and in the latest 3.8 release.

Image Editor basics

WordPress has two built in classes that handle image manipulation:

  • WP_Image_Editor_GD (/wp-includes/class-wp-image-editor-gd.php)
  • WP_Image_Editor_Imagick (/wp-includes/class-wp-image-editor-imagick.php)

These two classes extend WP_Image_Editor because they both use a different image engine (GD and ImageMagick respectively) to load, resize, compress and save images.

By default WordPress will try to use the ImageMagick engine first, which needs a PHP extension, because it is generally preferred over PHP’s default GD engine. Most shared servers don’t have the ImageMagick extension enabled though.

Add an Image Editor

To decide which engine to use, WordPress calls an internal function __wp_image_editor_choose() (located in /wp-includes/media.php). This function loops through all engines to see which engine can handle the request.

The function also has a filter called wp_image_editors that allows you to add more image editors like so:

add_filter("wp_image_editors", "my_wp_image_editors");
function my_wp_image_editors($editors) {
    array_unshift($editors, "WP_Image_Editor_Custom");

    return $editors;
}

Note we’re prepending our custom image editor class WP_Image_Editor_Custom so WordPress will check if our engine can handle resizing before testing other engines.

Creating our Image Editor

Now we’re gonna write our own image editor so we can decide on filenames for ourselves. Filenaming is handled by the method WP_Image_Editor::generate_filename() (both engines inherit this method), so we should overwrite that in our custom class.

Since we only plan on changing filenames, we should extend one of the existing engines so we don’t have to reinvent the wheel. I will extend WP_Image_Editor_GD in my example, as you probably don’t have the ImageMagick extension enabled. The code is interchangeable for an ImageMagick setup though. You could add both if you’re planning on using the theme on different setups.

// Include the existing classes first in order to extend them.
require_once ABSPATH.WPINC."/class-wp-image-editor.php";
require_once ABSPATH.WPINC."/class-wp-image-editor-gd.php";

class WP_Image_Editor_Custom extends WP_Image_Editor_GD {
    public function generate_filename($prefix = NULL, $dest_path = NULL, $extension = NULL) {
        // If empty, generate a prefix with the parent method get_suffix().
        if(!$prefix)
            $prefix = $this->get_suffix();

        // Determine extension and directory based on file path.
        $info = pathinfo($this->file);
        $dir  = $info['dirname'];
        $ext  = $info['extension'];

        // Determine image name.
        $name = wp_basename($this->file, ".$ext");

        // Allow extension to be changed via method argument.
        $new_ext = strtolower($extension ? $extension : $ext);

        // Default to $_dest_path if method argument is not set or invalid.
        if(!is_null($dest_path) && $_dest_path = realpath($dest_path))
            $dir = $_dest_path;

        // Return our new prefixed filename.
        return trailingslashit($dir)."{$prefix}/{$name}.{$new_ext}";
    }
}

Most of the code above was directly copied from the WP_Image_Editor class and commented for your convenience. The only actual change is that the suffix is now a prefix.

Alternatively, you could just call parent::generate_filename() and use an mb_str_replace() to change the suffix into a prefix, but I figured that would be more inclined to go wrong.

Saving new paths to metadata

After uploading image.jpg, the uploads folder looks like this:

  • 2013/12/150x150/image.jpg
  • 2013/12/300x300/image.jpg
  • 2013/12/image.jpg

So far so good. However, when calling basic functions like wp_get_attachment_image_src(), we’ll notice all image sizes are stored as image.jpg without the new directory path.

We can work around this issue by saving the new folder structure to the image metadata (where the filenames are stored). The data runs through various filters (wp_generate_attachment_metadata among others) before being inserted into the database, but since we’re already implementing a custom image editor, we can travel back to the source of image size metadata: WP_Image_Editor::multi_resize(). It generates arrays like this one:

Array (
    [thumbnail] => Array (
        [file]      => image.jpg
        [width]     => 150
        [height]    => 150
        [mime-type] => image/jpeg
    )

    [medium] => Array (
        [file]      => image.jpg
        [width]     => 300
        [height]    => 300
        [mime-type] => image/jpeg
    )
)

We’ll overwrite the multi_resize() method in our custom class:

function multi_resize($sizes) {
    $sizes = parent::multi_resize($sizes);

    foreach($sizes as $slug => $data)
        $sizes[$slug]['file'] = $data['width']."x".$data['height']."https://wordpress.stackexchange.com/".$data['file'];

    return $sizes;
}

As you can see, I didn’t bother replacing any of the code. I just call the parent method and let it generate the metadata. Then I loop through the resulting array and adjust the file value for each size.

Now wp_get_attachment_image_src($att_id, array(300, 300)) returns 2013/12/300x300/image.jpg. Hooray!

Final thoughts

I hope this provided a good basis for you to elaborate on. However, please note if an image is smaller than the specified size (e.g. 280×300), the generated suffix (prefix in our case) and image sizes are 280×300, not 300×300. If you upload a lot of smaller images, you’ll get a lot of different folders.

A good solution would be to either use the size slug as a folder name (small, medium, et cetera) or expand the code to round sizes up to the nearest preferred image size.

You noted you want to use just the width as a directory name. Be warned though – plugins or themes could generate two different sizes with the same width but different heights.

Also, you can remove the year/month folders either by disabling ‘Organize my uploads into month- and year-based folders’ under Settings > Media or by manipulating generate_filename even further.

Hope this helps. Good luck!

Leave a Comment