Randomise upload filenames (or another solution to hide the original image URL from theft?)

Where am I going wrong?

Actually, your code is just fine.

Except that I would simply use wp_generate_password() to generate the random string. 🙂

WordPress will still think that the scaled version is called
painting-scaled.jpg , etc. Resulting in broken images.

Yes, that’s correct.

Images are stored as posts of the attachment type in the WordPress posts table (in the database), and the details (width, height, full file path, etc.) of the main image and its intermediate sizes (e.g. thumbnail and medium) are stored in a (private) meta named _wp_attachment_metadata — see wp_generate_attachment_metadata() and wp_get_attachment_metadata() for more info, but the meta value basically consists of:

  • ‘width’
    (int) The width of the attachment.
  • ‘height’
    (int) The height of the attachment.
  • ‘file’
    (string) The file path relative to wp-content/uploads.
  • ‘sizes’
    (array) Keys are size slugs, each value is an array containing ‘file’, ‘width’, ‘height’, and ‘mime-type’.
  • ‘image_meta’
    (array) Image metadata.

And as for the -scaled version, if available, it will be used with the file item above and there’ll be an additional item named original_image added to the above meta where the item’s value will be the path to the originally uploaded image file — see wp_get_original_image_path().

So because the image_make_intermediate_size filters are applied both when generating the -scaled version and the intermediate sizes, then the -scaled version also gets renamed by your custom filter (the rename_intermediates() function), but unlike the above sizes item, WordPress no longer updates the file item once it is set after the -scaled version is generated.

Therefore that resulted in the broken image for the -scaled version.

But don’t fret, there’s a solution. 🙂

Yes, and basically you just need to use a different hook: wp_generate_attachment_metadata.

So just remove your image_make_intermediate_size filter and use the code below, and just let me know if you need any clarification:

// *UPDATED Feb 27th 2021 UTC

// Renames a file and if it succeeded, returns an array with the first item being the new name
// (e.g. <random string>-my-image.jpg). And this function is intended to be used only with the
// my_rename_images() below.
function my_rename_image( $file, $_rand_str="" ) {
    $info = pathinfo( $file );

    $rand_str = wp_generate_password( 8, false ); // false = do not use special chars
    $new_name = $rand_str . '-' . ( $_rand_str ?
        str_replace( "{$_rand_str}-", '', $info['basename'] ) : $info['basename'] );
    $new_file = $info['dirname'] . "https://wordpress.stackexchange.com/" . $new_name;

    return rename( $file, $new_file ) ? array( $new_name, $rand_str ) : false;
}

// Note: The _main_rand_str meta is used to prevent re-adding the random string to the name of
// the renamed image files.
add_filter( 'wp_generate_attachment_metadata', 'my_rename_images', 10, 3 );
function my_rename_images( $metadata, $attachment_id, $context ) {
    // Do nothing if the attachment is NOT an image, or if the attachment wasn't being
    // created.
    if ( 'create' !== $context || empty( $metadata['image_meta'] ) ) {
        return $metadata;
    }

    $file = get_attached_file( $attachment_id );
    $_rand_str = get_post_meta( $attachment_id, '_main_rand_str', true );

    if ( $arr = my_rename_image( $file, $_rand_str ) ) {
        $metadata['file'] = dirname( $metadata['file'] ) . "https://wordpress.stackexchange.com/" . $arr[0];
        update_attached_file( $attachment_id, $metadata['file'] );
        update_post_meta( $attachment_id, '_main_rand_str', $arr[1] );
    }

    $dir = dirname( $file );
    foreach ( $metadata['sizes'] as $size => $data ) {
        if ( $arr = my_rename_image( $dir . "https://wordpress.stackexchange.com/" . $data['file'], $_rand_str ) ) {
            $metadata['sizes'][ $size ]['file'] = $arr[0];
        }
    }

    if ( ! empty( $metadata['original_image'] ) ) {
        $file = $dir . "https://wordpress.stackexchange.com/" . $metadata['original_image'];
        if ( $arr = my_rename_image( $file, $_rand_str ) ) {
            $metadata['original_image'] = $arr[0];
            update_post_meta( $attachment_id, '_main_rand_str', $arr[1] );
        }
    }

    return $metadata;
}

Sample output

So I uploaded a hi-res JPEG image via the Media → Add New admin page, and the generated file names/sizes are:

| File Name                             | Size Name      | Note                    | 
|---------------------------------------|----------------|-------------------------| 
| 768O4Y7q-foo-bar-image-scaled.jpg     |                | The "-scaled" version   | 
| zDQdlxMK-foo-bar-image-300x200.jpg    | medium         |                         | 
| 6CT8XZGI-foo-bar-image-1024x683.jpg   | large          |                         | 
| BFcUGvP8-foo-bar-image-150x150.jpg    | thumbnail      |                         | 
| 2vATG8xN-foo-bar-image-768x512.jpg    | medium_large   |                         | 
| 8xjeou4f-foo-bar-image-1536x1024.jpg  | 1536x1536      |                         | 
| 59YJJsQF-foo-bar-image-2048x1366.jpg  | 2048x2048      |                         | 
| 6HjAlOnr-foo-bar-image-1568x1046.jpg  | post-thumbnail |                         | 
| XQTkPM7I-foo-bar-image.jpg            |                | Original, but renamed   | 

UPDATE

In response to “Edit 27th Feb 2021” in the question:

  1. The new smaller size locations aren’t all being updated” — this didn’t seem to have happened in my case, so please try the updated code above and see if the issue persists.

  2. Image regeneration doesn’t delete the old generated smaller sizes.” — yes, because as you quoted it yourself, WordPress core says, “it does not check or delete intermediate sizes it previously created for the same image“.

    So I’m afraid you’ll need to either manually delete the existing intermediate sizes, or find a plugin which can help you delete them. But note that one good reason why WordPress doesn’t delete the intermediate sizes, is because those images might be in use in that they were linked to from somewhere (which is not a thief..), so simply deleting the images is not a good idea.

  3. Sorry about the random string which got re-added to the name of the renamed files.. But I’ve corrected it in the updated code above.