WordPress.org

Make WordPress Core

Opened 16 months ago

Last modified 3 weeks ago

#40415 new enhancement

Imagick resize filter hook

Reported by: virgodesign Owned by:
Milestone: 5.0 Priority: normal
Severity: normal Version: 4.7.3
Component: Media Keywords: has-patch needs-testing
Focuses: Cc:

Description (last modified by audrasjb)

As of ticket #33642, WordPress 4.5 introduces the thumbnail_image() function within the WP_Image_Editor_Imagick class. This function uses the default imagick filter FILTER_TRIANGLE to assists the image resizing process. Within the thumbnail_image() is even declared a list of allowed filters that actually are absolute unsued due to the fact that wordpress uses only the default filter FILTER_TRIANGLE. The filter list is

'FILTER_POINT'
'FILTER_BOX'
'FILTER_TRIANGLE'
'FILTER_HERMITE'
'FILTER_HANNING'
'FILTER_HAMMING'
'FILTER_BLACKMAN'
'FILTER_GAUSSIAN'
'FILTER_QUADRATIC'
'FILTER_CUBIC'
'FILTER_CATROM'
'FILTER_MITCHELL'
'FILTER_LANCZOS'
'FILTER_BESSEL'
'FILTER_SINC'

Resizing image using the default filter often gives as a result a softly blurry thumbnail image. this blur effect became more visible as the thumbnail size increase.

Using filter like FILTER_HAMMING or FILTER_SINC the blur effect disappear, giving a great resizing result.

To better improve the use of the resizing process and to give any users the ability to choose which type of filter to use, I think that could be implemented the following filter hook:

$filter_name = apply_filters('image_resize_filter', $filter_name);

that will give an opportunity to change this filter before the resizing process will perform. This filter should be implemented at the beginning (line 1) of the thumbnail_image() function. This way everyone can, optionally, change the imagick resize filter easily.

Attachments (3)

40415.diff (1.1 KB) - added by ajoah 5 weeks ago.
Change default imagick filter + add a WP filter
40415.1.diff (1.9 KB) - added by audrasjb 5 weeks ago.
Adds FILTER_HAMMING to default filter in the current fallback
40415.2.diff (2.9 KB) - added by losangelos 3 weeks ago.
Use a windowed resizing filter (Lanczos). Improve sharpening for better quality images. Use progressive compression for smaller size images.

Download all attachments as: .zip

Change History (12)

#1 @dpacmittal
8 months ago

+1 for this. Triangle is a really bad default filter. WP should provide a filter hook, or better default.

In the meanwhile I'm using this workaround: in your functions.php:

<?php
require_once('class-imagick-modded.php');     
if(class_exists('WP_Image_Editor_Imagick_Modded')){                                         
    // Force imagick modded
    add_filter('wp_image_editors', function($implementations){                              
    return ['WP_Image_Editor_Imagick_Modded'];                                          
    })
}    

class-imagick-modded.php:

<?php
<?php                                                                                                                                                                                                                                                                                                                                                                                     
/**                                                                                                                                                                                                                                                                                                                                                                                       
 * WordPress Imagick Image Editor                                                                                                                                                                                                                                                                                                                                                         
 *                                                                                                                                                                                                                                                                                                                                                                                        
 * @package WordPress                                                                                                                                                                                                                                                                                                                                                                     
 * @subpackage Image_Editor                                                                                                                                                                                                                                                                                                                                                               
 */                                                                                                                                                                                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                          
/**                                                                                                                                                                                                                                                                                                                                                                                       
 * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module                                                                                                                                                                                                                                                                                                         
 *                                                                                                                                                                                                                                                                                                                                                                                        
 * @since 3.5.0                                                                                                                                                                                                                                                                                                                                                                           
 *                                                                                                                                                                                                                                                                                                                                                                                        
 * @see WP_Image_Editor                                                                                                                                                                                                                                                                                                                                                                   
 */                                                                                                                                                                                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                          
if(!class_exists('WP_Image_Editor_Imagick'))                                                                                                                                                                                                                                                                                                                                              
    return;                                                                                                                                                                                                                                                                                                                                                                               
class WP_Image_Editor_Imagick_Modded extends WP_Image_Editor_Imagick {                                                                                                                                                                                                                                                                                                                    
/**                                                                                                                                                                                                                                                                                                                                                                                       
     * Efficiently resize the current image                                                                                                                                                                                                                                                                                                                                               
     *                                                                                                                                                                                                                                                                                                                                                                                    
     * This is a WordPress specific implementation of Imagick::thumbnailImage(),                                                                                                                                                                                                                                                                                                          
     * which resizes an image to given dimensions and removes any associated profiles.                                                                                                                                                                                                                                                                                                    
     *                                                                                                                                                                                                                                                                                                                                                                                    
     * @since 4.5.0                                                                                                                                                                                                                                                                                                                                                                       
     *                                                                                                                                                                                                                                                                                                                                                                                    
     * @param int    $dst_w       The destination width.                                                                                                                                                                                                                                                                                                                                  
     * @param int    $dst_h       The destination height.                                                                                                                                                                                                                                                                                                                                 
     * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.                                                                                                                                                                                                                                                                           
     * @param bool   $strip_meta  Optional. Strip all profiles, excluding color profiles, from the image. Default true.                                                                                                                                                                                                                                                                   
     * @return bool|WP_Error                                                                                                                                                                                                                                                                                                                                                              
     */                                                                                                                                                                                                                                                                                                                                                                                   
    protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_HAMMING', $strip_meta = true ) {                                                                                                                                                                                                                                                                           
        $allowed_filters = array(                                                                                                                                                                                                                                                                                                                                                         
            'FILTER_POINT',                                                                                                                                                                                                                                                                                                                                                               
            'FILTER_BOX',                                                                                                                                                                                                                                                                                                                                                                 
            'FILTER_TRIANGLE',                                                                                                                                                                                                                                                                                                                                                            
            'FILTER_HERMITE',                                                                                                                                                                                                                                                                                                                                                             
            'FILTER_HANNING',                                                                                                                                                                                                                                                                                                                                                             
            'FILTER_HAMMING',                                                                                                                                                                                                                                                                                                                                                             
            'FILTER_BLACKMAN',                                                                                                                                                                                                                                                                                                                                                            
            'FILTER_GAUSSIAN',                                                                                                                                                                                                                                                                                                                                                            
            'FILTER_QUADRATIC',                                                                                                                                                                                                                                                                                                                                                           
            'FILTER_CUBIC',                                                                                                                                                                                                                                                                                                                                                               
            'FILTER_CATROM',                                                                                                                                                                                                                                                                                                                                                              
            'FILTER_MITCHELL',                                                                                                                                                                                                                                                                                                                                                            
            'FILTER_LANCZOS',                                                                                                                                                                                                                                                                                                                                                             
            'FILTER_BESSEL',                                                                                                                                                                                                                                                                                                                                                              
            'FILTER_SINC',                                                                                                                                                                                                                                                                                                                                                                
        );                                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                                          
        /**                                                                                                                                                                                                                                                                                                                                                                               
         * Set the filter value if '$filter_name' name is in our whitelist and the related                                                                                                                                                                                                                                                                                                
         * Imagick constant is defined or fall back to our default filter.                                                                                                                                                                                                                                                                                                                
         */                                                                                                                                                                                                                                                                                                                                                                               
        if ( in_array( $filter_name, $allowed_filters ) && defined( 'Imagick::' . $filter_name ) ) {                                                                                                                                                                                                                                                                                      
            $filter = constant( 'Imagick::' . $filter_name );                                                                                                                                                                                                                                                                                                                             
        } else {                                                                                                                                                                                                                                                                                                                                                                          
            $filter = defined( 'Imagick::FILTER_HAMMING' ) ? Imagick::FILTER_HAMMING : false;                                                                                                                                                                                                                                                                                             
        }                                                                                                                                                                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                          
        /**                                                                                                                                                                                                                                                                                                                                                                               
         * Filters whether to strip metadata from images when they're resized.                                                                                                                                                                                                                                                                                                            
         *                                                                                                                                                                                                                                                                                                                                                                                
         * This filter only applies when resizing using the Imagick editor since GD                                                                                                                                                                                                                                                                                                       
         * always strips profiles by default.                                                                                                                                                                                                                                                                                                                                             
         *                                                                                                                                                                                                                                                                                                                                                                                
         * @since 4.5.0                                                                                                                                                                                                                                                                                                                                                                   
         *                                                                                                                                                                                                                                                                                                                                                                                
         * @param bool $strip_meta Whether to strip image metadata during resizing. Default true.                                                                                                                                                                                                                                                                                         
         */                                                                                                                                                                                                                                                                                                                                                                               
        if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {                                                                                                                                                                                                                                                                                                                         
            $this->strip_meta(); // Fail silently if not supported.                                                                                                                                                                                                                                                                                                                       
        }                                                                                                                                                                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                          
        try {                                                                                                                                                                                                                                                                                                                                                                             
            /*                                                                                                                                                                                                                                                                                                                                                                            
             * To be more efficient, resample large images to 5x the destination size before resizing                                                                                                                                                                                                                                                                                     
             * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),                                                                                                                                                                                                                                                                                      
             * unless we would be resampling to a scale smaller than 128x128.                                                                                                                                                                                                                                                                                                             
             */                                                                                                                                                                                                                                                                                                                                                                           
            if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {                                                                                                                                                                                                                                                                                                                  
                $resize_ratio  = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );                                                                                                                                                                                                                                                                                  
                $sample_factor = 5;                                                                                                                                                                                                                                                                                                                                                       
                                                                                                                                                                                                                                                                                                                                                                                          
                if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {                                                                                                                                                                                                                                                                       
                    $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );                                                                                                                                                                                                                                                                                        
                }                                                                                                                                                                                                                                                                                                                                                                         
            }            
            /*                                                                                                                                                                                                                                                                                                                                                                            
             * Use resizeImage() when it's available and a valid filter value is set.                                                                                                                                                                                                                                                                                                     
             * Otherwise, fall back to the scaleImage() method for resizing, which                                                                                                                                                                                                                                                                                                        
             * results in better image quality over resizeImage() with default filter                                                                                                                                                                                                                                                                                                     
             * settings and retains backward compatibility with pre 4.5 functionality.                                                                                                                                                                                                                                                                                                    
             */                                                                                                                                                                                                                                                                                                                                                                           
            if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {                                                                                                                                                                                                                                                                                                       
                $this->image->setOption( 'filter:support', '2.0' );                                                                                                                                                                                                                                                                                                                       
                $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );                                                                                                                                                                                                                                                                                                                  
            } else {                                                                                                                                                                                                                                                                                                                                                                      
                $this->image->scaleImage( $dst_w, $dst_h );                                                                                                                                                                                                                                                                                                                               
            }                                                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                          
            // Set appropriate quality settings after resizing.                                                                                                                                                                                                                                                                                                                           
            if ( 'image/jpeg' == $this->mime_type ) {                                                                                                                                                                                                                                                                                                                                     
                                                                                                                                                                                                                                                                                                                                                                                          
                    $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );                                                                                                                                                                                                                                                                                                               
                }                                                                                                                                                                                                                                                                                                                                                                         
                                                                                                                                                                                                                                                                                                                                                                                          
                $this->image->setOption( 'jpeg:fancy-upsampling', 'off' );                                                                                                                                                                                                                                                                                                                
            }                                                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                          
            if ( 'image/png' === $this->mime_type ) {                                                                                                                                                                                                                                                                                                                                     
                $this->image->setOption( 'png:compression-filter', '5' );                                                                                                                                                                                                                                                                                                                 
                $this->image->setOption( 'png:compression-level', '9' );                                                                                                                                                                                                                                                                                                                  
                $this->image->setOption( 'png:compression-strategy', '1' );                                                                                                                                                                                                                                                                                                               
                $this->image->setOption( 'png:exclude-chunk', 'all' );                                                                                                                                                                                                                                                                                                                    
            }                                                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                          
            /*                                                                                                                                                                                                                                                                                                                                                                            
             * If alpha channel is not defined, set it opaque.                                                                                                                                                                                                                                                                                                                            
             *                                                                                                                                                                                                                                                                                                                                                                            
             * Note that Imagick::getImageAlphaChannel() is only available if Imagick                                                                                                                                                                                                                                                                                                     
             * has been compiled against ImageMagick version 6.4.0 or newer.                                                                                                                                                                                                                                                                                                              
             */                                                                                                                                                                                                                                                                                                                                                                           
            if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )                                                                                                                                                                                                                                                                                                             
                && is_callable( array( $this->image, 'setImageAlphaChannel' ) )                                                                                                                                                                                                                                                                                                           
                && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )                                                                                                                                                                                                                                                                                                                           
                && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )                                                                                                                                                                                                                                                                                                                              
            ) {                                                                                                                                                                                                                                                                                                                                                                           
                if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {                                                                                                                                                                                                                                                                                         
                    $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );                                                                                                                                                                                                                                                                                                   
                }                                                                                                                                                                                                                                                                                                                                                                         
            }                                                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                          
            // Limit the bit depth of resized images to 8 bits per channel.                                                                                                                                                                                                                                                                                                               
            if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {                                                                                                                                                                                                                                                       
                if ( 8 < $this->image->getImageDepth() ) {                                                                                                                                                                                                                                                                                                                                
                    $this->image->setImageDepth( 8 );                                                                                                                                                                                                                                                                                                                                     
                }                                                                                                                                                                                                                                                                                                                                                                         
            }                                                                                                                                                                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                                                                                                                                                          
            if ( is_callable( array( $this->image, 'setInterlaceScheme' ) ) && defined( 'Imagick::INTERLACE_NO' ) ) {                                                                                                                                                                                                                                                                     
                $this->image->setInterlaceScheme( Imagick::INTERLACE_NO );                                                                                                                                                                                                                                                                                                                
            }                                                                                                                                                                                                                                                                                                                                                                             
        } catch ( Exception $e ) {                                                                                                                                                                                                                                                                                                                                                        
            return new WP_Error( 'image_resize_error', $e->getMessage() );                                                                                                                                                                                                                                                                                                                
        }                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                          

@ajoah
5 weeks ago

Change default imagick filter + add a WP filter

#2 @ajoah
5 weeks ago

  • Keywords has-patch added

I made a website to see differences between filters : https://test-im-filters.unikmedia.fr/

I think the filter HAMMING is the better even if it is a little bigger (sometimes).

So i made a first patch to use it.

@audrasjb
5 weeks ago

Adds FILTER_HAMMING to default filter in the current fallback

#3 @audrasjb
5 weeks ago

  • Description modified (diff)
  • Keywords needs-testing added
  • Milestone changed from Awaiting Review to 5.0

Hi!

I'd say we also need to add FILTER_HAMMING to default filter fallback. See 40415.1.diff :)

Cheers, Jb

Last edited 5 weeks ago by audrasjb (previous) (diff)

#4 @audrasjb
4 weeks ago

#44473 was marked as a duplicate.

@losangelos
3 weeks ago

Use a windowed resizing filter (Lanczos). Improve sharpening for better quality images. Use progressive compression for smaller size images.

#5 @losangelos
3 weeks ago

We have added a few images test results in a github page for anyone to see. The settings of method 6 (which are included in 40415.2.diff) produce a much sharper image with smaller size at the same time for jpg images uploaded to Media Library.

Resize: Lanczos
Sharpening: unsharpMask( Radius 1, Sigma 0.45, Amount 3, Threshold 0 )
Compression: Interlace_Plane
Quality: 60

https://greenpeace.github.io/planet4-imagestest-results/ Here you can see results for 2 different images in 3 different sizes.

  • Method 1 is the currently used one.
  • Method 6 seems to produce best quality and size (up to -47%) combination.
  • Method 7 (the one used in 40415.1.diff) produces lower quality resized jpg images and at the same time larger in size. This is more evident in retina-large size images.

You can find also info in imagemagick's website https://www.imagemagick.org/Usage/filter/#lanczos

Last edited 3 weeks ago by losangelos (previous) (diff)

#6 @joemcgill
3 weeks ago

Hi @losangelos – Thanks for sharing the planet4 image option. When we implemented the current solution in #33642 one of the constraints we needed to balance was the output quality against the resources consumed by each compression method in order to reduce/avoid resize failures on underpowered shared hosting.

I'd love to see some performance data based on the current recommended WordPress hosting requirements to see if we can improve the defaults.

This ticket was mentioned in Slack in #core-media by joemcgill. View the logs.


3 weeks ago

#8 @losangelos
3 weeks ago

Hi @joemcgill @audrasjb and thanks for the quick responds! We 'll be happy to look into performance/resources cost and provide you with data soon!

#9 @virgodesign
3 weeks ago

Not sure if we should change now the default imagick filter. This would not be backward compatible, and it will affect image quality creating differences between images. I think that the first thing to do is to provide a filter hook. Changing the default filter need more data and testing about image quality (for big and small thumbnails) as well as resource usage.

Note: See TracTickets for help on using tickets.