WordPress.org

Make WordPress Core

Opened 3 years ago

Last modified 6 months ago

#40415 new enhancement

Imagick resize filter hook

Reported by: virgodesign Owned by:
Milestone: Future Release Priority: normal
Severity: normal Version: 4.7.3
Component: Media Keywords: has-patch needs-testing reporter-feedback
Focuses: Cc:
PR Number:

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 (4)

40415.diff (1.1 KB) - added by ajoah 18 months ago.
Change default imagick filter + add a WP filter
40415.1.diff (1.9 KB) - added by audrasjb 18 months ago.
Adds FILTER_HAMMING to default filter in the current fallback
40415.2.diff (2.9 KB) - added by losangelos 18 months ago.
Use a windowed resizing filter (Lanczos). Improve sharpening for better quality images. Use progressive compression for smaller size images.
class-wp-image-editor-imagick.php.patch (658 bytes) - added by robertark 6 months ago.
Allow users to define Imagick resize filter

Download all attachments as: .zip

Change History (23)

#1 follow-up: @dpacmittal
2 years 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
18 months ago

Change default imagick filter + add a WP filter

#2 @ajoah
18 months 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
18 months ago

Adds FILTER_HAMMING to default filter in the current fallback

#3 @audrasjb
18 months 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 18 months ago by audrasjb (previous) (diff)

#4 @audrasjb
18 months ago

#44473 was marked as a duplicate.

@losangelos
18 months ago

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

#5 @losangelos
18 months 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 18 months ago by losangelos (previous) (diff)

#6 @joemcgill
18 months 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.


18 months ago

#8 @losangelos
18 months 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
18 months 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.

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


15 months ago

#11 @peterwilsoncc
14 months ago

  • Milestone changed from 5.0 to 5.1

Moving to the 5.1 milestone due to the WordPress 5.0 focus on the new
editor (Gutenberg).

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


12 months ago

#13 @pento
11 months ago

  • Keywords reporter-feedback added
  • Milestone changed from 5.1 to Future Release

#14 @pinktank
7 months ago

Hello all, since this has gotten postponed to future releases, is there a way to load a modified version of class-wp-image-editor-imagick.php without modding the original file in place. The difference in quality is quite ridiculuous so I really want to use my parameters as I mostly work with artists for their portfolios. The method above does not work to extend the class. I'd like to change the filter and sharpening both. Also on topic, can someone detail what is going on with the section that resizes the image to 5x the destination before resampling? Is it using a more efficient algorithm to make it to an intermediate size before resampling with more expensive algorithms?

@robertark
6 months ago

Allow users to define Imagick resize filter

#15 in reply to: ↑ 1 ; follow-up: @robertark
6 months ago

Replying to dpacmittal:

+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:
[... truncated ...]

Unfortunately, this workaround does not work (tested in 5.2.2).

Is it possible to get more attention to this? We have dozens of clients who are reporting that their resized images are blurry - this is not ideal, especially since our sites are hosted on platforms that can handle the expensive algorithms to result in sharp images.

I have submitted a diff to this issue (class-wp-image-editor-imagick.php.patch) which adds just the apply_filters call before processing the image resize function. This should allow for backwards compatibility as it does not change the existing filters or any of the other Imagick options.

#16 in reply to: ↑ 15 ; follow-up: @pinktank
6 months ago

Replying to robertark:

I have submitted a diff to this issue (class-wp-image-editor-imagick.php.patch) which adds just the apply_filters call before processing the image resize function. This should allow for backwards compatibility as it does not change the existing filters or any of the other Imagick options.

If you are going to add a filter for the resize function, we should add one for the sharpening that happens below as well since it does better with different sharpening parameters once you use a better resizing algorithm.

#17 in reply to: ↑ 16 @robertark
6 months ago

Replying to pinktank:

Replying to robertark:

I have submitted a diff to this issue (class-wp-image-editor-imagick.php.patch) which adds just the apply_filters call before processing the image resize function. This should allow for backwards compatibility as it does not change the existing filters or any of the other Imagick options.

If you are going to add a filter for the resize function, we should add one for the sharpening that happens below as well since it does better with different sharpening parameters once you use a better resizing algorithm.

Thank you for your reply - I agree. I am unsure of which or what parameters work best with, say Lanczos, but in my testing what was already defined in the unsharpMaskImage() appeared to be fine. How would you go about providing options for the sharpening?

PS: It's my understanding that there is no universal setting for sharpening, including any based on a specific algorithm. Digging into this scientific answer here is mind boggling to say the least, but it may be helpful to others: http://www.imagemagick.org/Usage/filter/nicolas/

This resource may also be helpful: http://www.imagemagick.org/discourse-server/viewtopic.php?f=22&t=25935

Last edited 6 months ago by robertark (previous) (diff)

#18 follow-up: @pinktank
6 months ago

I don't know what the best implementation would be but we could add variables like so, amount should be dropped a bit with lanczos 8>4 maybe , and threshold is better around 0.04

unsharpMaskImage( radius, sigma, amount, threshold );

#19 in reply to: ↑ 18 @robertark
6 months ago

Replying to pinktank:

I don't know what the best implementation would be but we could add variables like so, amount should be dropped a bit with lanczos 8>4 maybe , and threshold is better around 0.04

unsharpMaskImage( radius, sigma, amount, threshold );

I totally agree. Is it possible to have a filter that can control all 4 parameters? Or would it require 4 separate filters? Or does it make sense to control the image object in a filter and apply any Imagick functions there?

Note: See TracTickets for help on using tickets.