performance icon indicating copy to clipboard operation
performance copied to clipboard

PDF thumbnails to WEBP/AVIF

Open 1ucay opened this issue 1 year ago • 9 comments

Dear all,

if you upload PDF, in metadata sizes we have array with jpgs.

I tried fix with

add_filter( 'webp_uploads_upload_image_mime_transforms', 'core_webp_uploads_upload_image_mime_transforms', 9, 2 );
function core_webp_uploads_upload_image_mime_transforms( $default_transforms ) {

    $mimes = wp_get_mime_types();

    $output_format = webp_uploads_mime_type_supported( 'image/avif' ) ? webp_uploads_get_image_output_format() : 'webp';
    $transforms    = webp_uploads_is_fallback_enabled() ? array( 'image/jpeg', 'image/' . $output_format ) : array( 'image/' . $output_format );

    foreach( $mimes as $k => $v ) {
        if ( ! isset( $default_transforms[ $v ] ) )
            $default_transforms[ $v ] = $transforms;
    }

    return $default_transforms;
}

If you upload PSD with mime image/vnd.adobe.photoshop and genereate sizes with some custom code (imagemagick support PSD) like PDF does

add_filter( 'wp_generate_attachment_metadata', 'core_wp_generate_attachment_metadata', 9, 2 );

it working ok. I have nice jpg and avif in metadata.

But, different situation is with PDF mime application/pdf. It is blocked by helper.php line 145

$image_path = wp_get_original_image_path( $attachment_id );

and inside function is condition

  if ( ! wp_attachment_is_image( $attachment_id ) ) {

We dont have any filter skip this check, so PDF avif is not working. Best would be add filter to "wp_attachment_is" and override.

Im working on LibreOffice (exec soffice), audiowave (exec https://github.com/bbc/audiowaveform ) , PSD / AI (imagemagick) thumbnailer. https://core.trac.wordpress.org/ticket/62712

1ucay avatar Dec 20 '24 21:12 1ucay

Hey @1ucay

If I understand correctly, the issue is that PDF uploads should generate previews, but generation is blocked because wp_get_original_image_path() checks wp_attachment_is_image(), which returns false for PDFs.

I did try to replicate the issue and did not get pdf previews but I was unable to confirm if that is due to ! wp_attachment_is_image( $attachment_id ).

If you could revisit the issue once more and provide replication instructions, it would be very helpful.

AhmarZaidi avatar Feb 07 '25 13:02 AhmarZaidi

Dear @AhmarZaidi yes, exactly, for AVIF previews it is blocked by wp_attachment_is_image, if mime type of file is not image/ Would be possible add some apply_filters?

1ucay avatar Feb 08 '25 12:02 1ucay

you can add application/pdf over webp_uploads_upload_image_mime_transforms

1ucay avatar Feb 08 '25 12:02 1ucay

@1ucay Thanks for the confirmation.

There already exist a filter webp_uploads_upload_image_mime_transforms which we can use to add application/pdf mimetype to transforms like so:

add_filter( 'webp_uploads_upload_image_mime_transforms', function( $transforms ) {
    $transforms['application/pdf'] = array( 'image/jpeg' );
    return $transforms;
});

A possible solution could be to add application/pdf mimetype to the default list and use get_attached_file() function for getting the original pdf file path if mime type is application/pdf to bypass $image_path = wp_get_original_image_path( $attachment_id );


Now, it doesn't work when I have the dev environment running using npm run wp-env start and shows security policy error:

WP_Error Object
(
    [errors] => Array
    (
        [invalid_image] => Array
        (
            [0] => attempt to perform an operation not allowed by the security policy `PDF' @ error/constitute.c/IsCoderAuthorized/426
        )
    )

    [error_data] => Array
    (
        [invalid_image] => /var/www/html/wp-content/uploads/2025/02/test.pdf
    )

    [additional_data:protected] => Array ()
)

This appears to be an issue with ImageMagick policy used in the docker WordPress environment. Solutions suggest changing the ImageMagick policy

Image

However, looking into the core file reveal that it should work so I tested the solution on other setups like localWP (by separately adding Modern Image Formats plugin) and it seems to work fine.

https://github.com/user-attachments/assets/9a4f940f-3860-43e3-9aa2-4e252e10ff34

Feel free to let me know if I've understood or implemented something incorrectly.

AhmarZaidi avatar Feb 12 '25 11:02 AhmarZaidi

I have some workaround for this. but it so hacky ;) I can upload even PSD file (image/vnd.adobe.photoshop), which is by ImageMagick supported format. It will create thumbnail sizes (jpg,avif), I can even work in WP image editor (cropping etc). But it will be separate plugin.

Best solution for every usecase would be add filter to wp_attachment_is_image. So user can override function.

You have to change post_mime_type in WP Post object after filter "webp_uploads_pre_generate_additional_image_source"

<?php

if ( ! defined( 'ABSPATH' ) ) {
    die( 'Cannot access pages directly.' );
}

function core_get_image_viewable_formats() {
    return array( 'image/jpeg', 'image/png', 'image/apng', 'image/gif', 'image/bmp', 'image/webp', 'image/avif' );
}

// important hook with priority 10!! otherwise not working second image edit
add_filter( 'wp_update_attachment_metadata', 'core_thumbnail_generator_wp_update_attachment_metadata', 10, 2 );
function core_thumbnail_generator_wp_update_attachment_metadata( $meta, $attachment_id ) {

    if ( ! did_action( 'core_thumbnail_generator_alter_post_mime_type' ) )
        return $meta;

    $old_meta = wp_get_attachment_metadata( $attachment_id );

    $meta['sizes']['full'] = array(
        'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $meta['file'] ) ),
        'width'     => $meta['width'],
        'height'    => $meta['height'],
        'mime-type' => $meta['sizes']['full']['mime-type'],
        'filesize'  => $meta['filesize']
    );

    $meta['width']    = $old_meta['width'];
    $meta['height']   = $old_meta['height'];
    $meta['filesize'] = $old_meta['filesize'];

    return $meta;
}

// fix for AVIF / WEBP for Modern Image Formats
add_filter( 'webp_uploads_upload_image_mime_transforms', function( $transforms ) {
    $transforms['application/pdf'] = array( 'image/jpeg' );
    return $transforms;
});

add_filter( 'webp_uploads_pre_generate_additional_image_source', 'core_thumbnail_generator_webp_uploads_pre_generate_additional_image_source', 10, 5 );
function core_thumbnail_generator_webp_uploads_pre_generate_additional_image_source( $return, $attachment_id, $image_size, $size_data, $mime ) {

    core_thumbnail_generator_alter_post_mime_type( $attachment_id, false );

    return;
}

add_filter( 'wp_update_attachment_metadata', 'core_thumbnail_generator_wp_update_attachment_metadata_webp', 11, 2 );
function core_thumbnail_generator_wp_update_attachment_metadata_webp( $meta, $attachment_id ) {

    if ( did_filter( 'webp_uploads_upload_image_mime_transforms' ) && isset( $meta['sources'] ) && isset( $meta['sizes'] ) && isset( $meta['sizes']['full'] ) ) {

        // revert alter mime
        $attachment = wp_cache_get( $attachment_id, 'posts' );

        if ( isset( $attachment->post_mime_type_backup ) ) {

            $attachment->post_mime_type = $attachment->post_mime_type_backup;
            unset( $attachment->post_mime_type_backup );
            wp_cache_set( $attachment->ID, $attachment, 'posts' );

            if ( isset( $meta['sources'][ $attachment->post_mime_type ] ) ) {

                unset( $meta['sources'][ $attachment->post_mime_type ] );

                $meta['sources'][ $meta['sizes']['full']['mime-type'] ] = array(
                    'file'     => $meta['sizes']['full']['file'],
                    'filesize' => $meta['sizes']['full']['filesize'],
                );

            }

            remove_filter( 'get_attached_file', 'core_thumbnail_generator_get_attached_file', 10, 2 );

        }
    }

    return $meta;
}

function core_thumbnail_generator_update_attached_file( $file, $attachment_id ) {

    add_filter( 'update_post_metadata', function( $return, $object_id, $meta_key, $meta_value, $prev_value ) {

        remove_filter( current_filter(), __FUNCTION__ );

        if ( '_wp_attached_file' === $meta_key )
            return true;

    }, 10, 5 );

    return get_post_meta( $attachment_id, '_wp_attached_file', true );

}

function core_thumbnail_generator_alter_post_mime_type( $attachment_id = null ) {

    $post = get_post( $attachment_id );

    $attachment = wp_cache_get( $attachment_id, 'posts' );

    if ( ! $attachment || is_a( $attachment, 'WP_Post' ) )
        return;

    if ( in_array( $attachment->post_mime_type, core_get_image_viewable_formats() ) )
        return;

    $sizes = image_get_intermediate_size( $attachment->ID, 'full' );
    if ( ! $sizes )
        return;

    $attachment->post_mime_type_backup = $attachment->post_mime_type;
    $attachment->post_mime_type = $sizes['mime-type'];

    wp_cache_set( $attachment->ID, $attachment, 'posts' );

    add_filter( 'get_attached_file', 'core_thumbnail_generator_get_attached_file', 10, 2 );
    
    do_action( 'core_thumbnail_generator_alter_post_mime_type' );
}

function core_thumbnail_generator_get_attached_file( $filepath, $attachment_id ) {

    if ( $full_size = image_get_intermediate_size( $attachment_id, 'full' ) )
        $filepath = path_join( dirname( $filepath ), $full_size['file'] );

    return $filepath;
}


// bug wordpress-develop/src/wp-includes/media.php @4613, psd is image, but cannot be editable
add_filter( 'wp_prepare_attachment_for_js', 'core_thumbnail_generator_wp_prepare_attachment_for_js', 10, 3 );
function core_thumbnail_generator_wp_prepare_attachment_for_js( $response, $attachment, $meta ) {

    if ( ! empty( $meta['sizes'] ) && ( isset( $attachment->post_mime_type_backup ) || ! in_array( $attachment->post_mime_type, core_get_image_viewable_formats() ) ) ) {

        $attachment_url = wp_get_attachment_url( $attachment->ID );
        $base_url       = str_replace( wp_basename( $attachment_url ), '', $attachment_url );

        if ( $full_size = image_get_intermediate_size( $attachment->ID, 'full' ) ) {
            $response['sizes']['full'] = array(
                'url'         => $base_url . $full_size['file'] . ( str_contains( wp_get_referer(), '&mode=edit' ) ? '?' . time() . rand( 100, 999 ) : '' ),
                'height'      => $full_size['height'],
                'width'       => $full_size['width'],
                'orientation' => $full_size['height'] > $full_size['width'] ? 'portrait' : 'landscape',
            );
            $response['editable'] = true;
            return $response;
        }
    }


    return $response;
}

1ucay avatar Feb 12 '25 12:02 1ucay

I've created a draft PR with the previous approach before noticing there's an update 😅:

A possible solution could be to add application/pdf mimetype to the default list and use get_attached_file() function for getting the original pdf file path if mime type is application/pdf to bypass $image_path = wp_get_original_image_path( $attachment_id );

Will be reviewing and testing the new approach:

Best solution for every usecase would be add filter to wp_attachment_is_image. So user can override function.

AhmarZaidi avatar Feb 12 '25 13:02 AhmarZaidi

Nice, thank you, but it is solution only for PDF :) What about, if I have thumbnail for different type of document, for example docx, waveform of mp3 etc?

My proposal

$image_path = in_array( get_post_mime_type( $attachment_id ), array( 'image/jpeg', 'image/webp', 'image/avif', 'image/png' ) ) ? wp_get_original_image_path( $attachment_id ) : get_attached_file( $attachment_id );

1ucay avatar Feb 12 '25 13:02 1ucay

I've updated the logic like this:

$image_path = wp_attachment_is_image( $attachment_id ) ? wp_get_original_image_path( $attachment_id ) : get_attached_file( $attachment_id );

This way we can use wp_get_original_image_path() only when attatchment is an image and in all other cases we can use get_attached_file()


Also, since the original issue name and description mainly focuses on PDF incompatibility, that was the main focus of this PR. To handle all the cases like docx, waveform of mp3, etc., we would have to probabily follow something like the approach you suggested here.

And as far as my understanding goes, we won't be able to directly add filter to wp_attachment_is_image since it's a wp core function, instead we would have to create a wrapper function which uses wp_attachment_is_image().

AhmarZaidi avatar Feb 13 '25 12:02 AhmarZaidi

Thank you, sir !

1ucay avatar Feb 13 '25 14:02 1ucay