[wp-trac] [WordPress Trac] #39262: Fall back to ImageMagick command line when the pecl imagic is not available on the server

WordPress Trac noreply at wordpress.org
Wed Dec 14 01:49:12 UTC 2016


#39262: Fall back to ImageMagick command line when the pecl imagic is not available
on the server
--------------------------------+------------------------------
 Reporter:  Hristo Sg           |       Owner:
     Type:  enhancement         |      Status:  new
 Priority:  normal              |   Milestone:  Awaiting Review
Component:  External Libraries  |     Version:  4.7
 Severity:  normal              |  Resolution:
 Keywords:                      |     Focuses:
--------------------------------+------------------------------

Old description:

> The patch allows WordPress to fall back to the ImageMagick command line
> when the imagic pecl is not available on the server. Patch attached.
> Here's the .diff:
>

> {{{
> diff --git a/wp-includes/class-wp-image-editor-imagick-external.php b/wp-
> includes/class-wp-image-editor-imagick-external.php
> new file mode 100644
> index 0000000..5f61280
> --- /dev/null
> +++ b/wp-includes/class-wp-image-editor-imagick-external.php
> @@ -0,0 +1,407 @@
> +<?php
> +/**
> + * WordPress Imagick Image Editor
> + *
> + * @package WordPress
> + * @subpackage Image_Editor
> + */
> +
> +/**
> + * WordPress Image Editor Class for Image Manipulation through Imagick
> command line utilities
> + *
> + * @since 3.5.0
> + * @package WordPress
> + * @subpackage Image_Editor
> + * @uses WP_Image_Editor Extends class
> + */
> +class WP_Image_Editor_Imagick_External extends WP_Image_Editor {
> +       /**
> +        * Imagick object.
> +        *
> +        * @access protected
> +        * @var Imagick
> +        */
> +       protected $image;
> +       public static $prog_convert  = '/usr/bin/convert';
> +       public static $prog_identify = '/usr/bin/identify';
> +
> +       public function __destruct() {
> +       }
> +
> +       /**
> +        * Checks to see if current environment supports Imagick.
> +        *
> +        * We require Imagick 2.2.0 or greater, based on whether the
> queryFormats()
> +        * method can be called statically.
> +        *
> +        * @since 3.5.0
> +        *
> +        * @static
> +        * @access public
> +        *
> +        * @param array $args
> +        * @return bool
> +        */
> +       public static function test( $args = array() ) {
> +               return is_executable( self::$prog_convert ) &&
> is_executable( self::$prog_identify );
> +       }
> +
> +       /**
> +        * Checks to see if editor supports the mime-type specified.
> +        *
> +        * @since 3.5.0
> +        *
> +        * @static
> +        * @access public
> +        *
> +        * @param string $mime_type
> +        * @return bool
> +        */
> +       public static function supports_mime_type( $mime_type ) {
> +               $imagick_extension = strtoupper( self::get_extension(
> $mime_type ) );
> +
> +               if ( ! $imagick_extension )
> +                       return false;
> +
> +               if ( $imagick_extension === 'PDF' )
> +                       return true;
> +
> +               return false;
> +       }
> +
> +       /**
> +        * Loads image from $this->file into new Imagick Object.
> +        *
> +        * @since 3.5.0
> +        * @access protected
> +        *
> +        * @return true|WP_Error True if loaded; WP_Error on failure.
> +        */
> +       public function load() {
> +               if ( $this->image )
> +                       return true;
> +
> +               $this->image = $this->file;
> +
> +               if ( ! is_file( $this->file ) )
> +                       return new WP_Error( 'error_loading_image',
> __('File doesn’t exist?'), $this->file );
> +
> +               try {
> +                       $filename = $this->file;
> +
> +                       list( $filename, $extension, $mime_type ) =
> $this->get_output_format( $filename );
> +                       $this->mime_type = $mime_type;
> +               }
> +               catch ( Exception $e ) {
> +                       return new WP_Error( 'invalid_image',
> $e->getMessage(), $this->file );
> +               }
> +
> +               $updated_size = $this->update_size();
> +               if ( is_wp_error( $updated_size ) ) {
> +                       return $updated_size;
> +               }
> +
> +               return $this->set_quality();
> +       }
> +
> +       /**
> +        * Sets Image Compression quality on a 1-100% scale.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param int $quality Compression Quality. Range: [1,100]
> +        * @return true|WP_Error True if set successfully; WP_Error on
> failure.
> +        */
> +       public function set_quality( $quality = null ) {
> +               $quality_result = parent::set_quality( $quality );
> +               if ( is_wp_error( $quality_result ) ) {
> +                       return $quality_result;
> +               } else {
> +                       $quality = $this->get_quality();
> +               }
> +               return true;
> +       }
> +
> +       /**
> +        * Sets or updates current image size.
> +        *
> +        * @since 3.5.0
> +        * @access protected
> +        *
> +        * @param int $width
> +        * @param int $height
> +        *
> +        * @return true|WP_Error
> +        */
> +       protected function update_size( $width = null, $height = null ) {
> +               $size = null;
> +               if ( !$width || !$height ) {
> +                       try {
> +                               $ret = shell_exec( self::$prog_identify .
> " -format '%[width] %[height]' " .  escapeshellarg( $this->file ) . "
> 2>/dev/null" );
> +                               list( $width, $height ) = explode( " ",
> trim( $ret ) );
> +                       }
> +                       catch ( Exception $e ) {
> +                               return new WP_Error( 'invalid_image', __(
> 'Could not read image size.' ), $this->file );
> +                       }
> +               }
> +               if ( ! $width )
> +                       $width = $size['width'];
> +               if ( ! $height )
> +                       $height = $size['height'];
> +
> +               return parent::update_size( $width, $height );
> +       }
> +
> +       /**
> +        * Resizes current image.
> +        *
> +        * At minimum, either a height or width must be provided.
> +        * If one of the two is set to null, the resize will
> +        * maintain aspect ratio according to the provided dimension.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param  int|null $max_w Image width.
> +        * @param  int|null $max_h Image height.
> +        * @param  bool     $crop
> +        * @return bool|WP_Error
> +        */
> +       public function resize( $max_w, $max_h, $crop = false ) {
> +               if ( ( $this->size['width'] == $max_w ) && (
> $this->size['height'] == $max_h ) )
> +                       return true;
> +
> +               $dims = image_resize_dimensions( $this->size['width'],
> $this->size['height'], $max_w, $max_h, $crop );
> +               if ( ! $dims )
> +                       return new WP_Error( 'error_getting_dimensions',
> __('Could not calculate resized image dimensions') );
> +               list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h,
> $src_w, $src_h ) = $dims;
> +
> +               if ( $crop ) {
> +                       return $this->crop( $src_x, $src_y, $src_w,
> $src_h, $dst_w, $dst_h );
> +               }
> +
> +               // Execute the resize
> +               $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
> +               if ( is_wp_error( $thumb_result ) ) {
> +                       return $thumb_result;
> +               }
> +
> +               return $this->update_size( $dst_w, $dst_h );
> +       }
> +
> +       /**
> +        * 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
> +        * @access protected
> +        *
> +        * @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_TRIANGLE', $strip_meta = true ) {
> +               list( $filename, $extension, $mime_type ) =
> $this->get_output_format( $this->filename, $this->mime_type );
> +               $dst_w = intval( $dst_w );
> +               $dst_h = intval( $dst_h );
> +               $filename = $this->generate_filename(
> "${dst_w}x${dst_h}", null, $extension );
> +
> +               $ret = 0;
> +               $cmd = self::$prog_convert .
> +                       " " . escapeshellarg( $this->file ) .
> +                       " -resize " . escapeshellarg( "${dst_w}x$dst_h" )
> .
> +                       " -quality " . escapeshellarg( $this->quality ).
> +                       " " . escapeshellarg( $filename );
> +
> +               system( $cmd, $ret );
> +               if ( $ret !== 0 )
> +                       return new WP_Error( 'image_resize_error',
> "convert returned error: $ret", $filename );
> +
> +               return true;
> +       }
> +
> +       /**
> +        * Resize multiple images from a single source.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param array $sizes {
> +        *     An array of image size arrays. Default sizes are 'small',
> 'medium', 'medium_large', 'large'.
> +        *
> +        *     Either a height or width must be provided.
> +        *     If one of the two is set to null, the resize will
> +        *     maintain aspect ratio according to the provided dimension.
> +        *
> +        *     @type array $size {
> +        *         Array of height, width values, and whether to crop.
> +        *
> +        *         @type int  $width  Image width. Optional if `$height`
> is specified.
> +        *         @type int  $height Image height. Optional if `$width`
> is specified.
> +        *         @type bool $crop   Optional. Whether to crop the
> image. Default false.
> +        *     }
> +        * }
> +        * @return array An array of resized images' metadata by size.
> +        */
> +       public function multi_resize( $sizes ) {
> +               $metadata = array();
> +               $orig_size = $this->size;
> +               $orig_image = $this->image;
> +
> +               foreach ( $sizes as $size => $size_data ) {
> +                       if ( ! $this->image )
> +                               $this->image = $orig_image;
> +
> +                       if ( ! isset( $size_data['width'] ) && ! isset(
> $size_data['height'] ) ) {
> +                               continue;
> +                       }
> +
> +                       if ( ! isset( $size_data['width'] ) ) {
> +                               $size_data['width'] = null;
> +                       }
> +                       if ( ! isset( $size_data['height'] ) ) {
> +                               $size_data['height'] = null;
> +                       }
> +
> +                       if ( ! isset( $size_data['crop'] ) ) {
> +                               $size_data['crop'] = false;
> +                       }
> +
> +                       $resize_result = $this->resize(
> $size_data['width'], $size_data['height'], $size_data['crop'] );
> +
> +                       $duplicate = ( ( $orig_size['width'] ==
> $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] )
> );
> +
> +                       if ( ! is_wp_error( $resize_result ) && !
> $duplicate ) {
> +                               $resized = $this->_save( $this->image );
> +                               $this->image = null;
> +                               if ( ! is_wp_error( $resized ) &&
> $resized ) {
> +                                       unset( $resized['path'] );
> +                                       $metadata[$size] = $resized;
> +                               }
> +                       }
> +
> +                       $this->size = $orig_size;
> +               }
> +
> +               $this->image = $orig_image;
> +
> +               return $metadata;
> +       }
> +
> +       /**
> +        * Crops Image.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param int  $src_x The start x position to crop from.
> +        * @param int  $src_y The start y position to crop from.
> +        * @param int  $src_w The width to crop.
> +        * @param int  $src_h The height to crop.
> +        * @param int  $dst_w Optional. The destination width.
> +        * @param int  $dst_h Optional. The destination height.
> +        * @param bool $src_abs Optional. If the source crop points are
> absolute.
> +        * @return bool|WP_Error
> +        */
> +       public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w =
> null, $dst_h = null, $src_abs = false ) {
> +               return new WP_Error( 'image_crop_error', 'Unsupported
> operation' );
> +       }
> +
> +       /**
> +        * Rotates current image counter-clockwise by $angle.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param float $angle
> +        * @return true|WP_Error
> +        */
> +       public function rotate( $angle ) {
> +               return new WP_Error( 'image_rotate_error', 'Unsupported
> operation' );
> +       }
> +
> +       /**
> +        * Flips current image.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param bool $horz Flip along Horizontal Axis
> +        * @param bool $vert Flip along Vertical Axis
> +        * @return true|WP_Error
> +        */
> +       public function flip( $horz, $vert ) {
> +               return new WP_Error( 'image_flip_error', 'Unsupported
> operation' );
> +       }
> +
> +       /**
> +        * Saves current image to file.
> +        *
> +        * @since 3.5.0
> +        * @access public
> +        *
> +        * @param string $destfilename
> +        * @param string $mime_type
> +        * @return array|WP_Error {'path'=>string, 'file'=>string,
> 'width'=>int, 'height'=>int, 'mime-type'=>string}
> +        */
> +       public function save( $destfilename = null, $mime_type = null ) {
> +               $saved = $this->_save( $this->image, $destfilename,
> $mime_type );
> +
> +               if ( ! is_wp_error( $saved ) ) {
> +                       $this->file = $saved['path'];
> +                       $this->mime_type = $saved['mime-type'];
> +               }
> +
> +               return $saved;
> +       }
> +
> +       /**
> +        *
> +        * @param Imagick $image
> +        * @param string $filename
> +        * @param string $mime_type
> +        * @return array|WP_Error
> +        */
> +       protected function _save( $image, $filename = null, $mime_type =
> null ) {
> +               list( $filename, $extension, $mime_type ) =
> $this->get_output_format( $filename, $mime_type );
> +
> +               if ( ! $filename )
> +                       $filename = $this->generate_filename( null, null,
> $extension );
> +
> +               $ret = 0;
> +               $cmd = self::$prog_convert .
> +                       " " . escapeshellarg( $this->file ) .
> +                       " -quality " . escapeshellarg($this->quality) .
> +                       " +repage" .
> +                       " " . escapeshellarg( $filename );
> +               system( $cmd, $ret );
> +               if ( $ret !== 0 )
> +                       return new WP_Error( 'image_save_error', "convert
> returned error: $ret", $filename );
> +
> +               // Set correct file permissions
> +               $stat = stat( dirname( $filename ) );
> +               $perms = $stat['mode'] & 0000666; //same permissions as
> parent folder, strip off the executable bits
> +               @ chmod( $filename, $perms );
> +
> +               /** This filter is documented in wp-includes/class-wp-
> image-editor-gd.php */
> +               return array(
> +                       'path'      => $filename,
> +                       'file'      => wp_basename( apply_filters(
> 'image_make_intermediate_size', $filename ) ),
> +                       'width'     => $this->size['width'],
> +                       'height'    => $this->size['height'],
> +                       'mime-type' => $mime_type,
> +               );
> +       }
> +
> +       public function stream( $mime_type = null ) {
> +               header( "Content-Type: $mime_type" );
> +               readfile( $this->file );
> +               return;
> +       }
> +
> +}
> diff --git a/wp-includes/media.php b/wp-includes/media.php
> index ba52555..a7aa3ad 100644
> --- a/wp-includes/media.php
> +++ b/wp-includes/media.php
> @@ -2928,15 +2928,17 @@ function _wp_image_editor_choose( $args = array()
> ) {
>         require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
>         require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
>         require_once ABSPATH . WPINC . '/class-wp-image-editor-
> imagick.php';
> +       require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick-
> external.php';
>         /**
>          * Filters the list of image editing library classes.
>          *
>          * @since 3.5.0
>          *
>          * @param array $image_editors List of available image editors.
> Defaults are
> -        *                             'WP_Image_Editor_Imagick',
> 'WP_Image_Editor_GD'.
> +        *                             'WP_Image_Editor_Imagick',
> 'WP_Image_Editor_GD', 'WP_Image_Editor_Imagick_External'
>          */
> -       $implementations = apply_filters( 'wp_image_editors', array(
> 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) );
> +       $implementations = apply_filters( 'wp_image_editors', array(
> 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD',
> +               'WP_Image_Editor_Imagick_External' ) );
>
>         foreach ( $implementations as $implementation ) {
>                 if ( ! call_user_func( array( $implementation, 'test' ),
> $args ) )
>
> }}}

New description:

 The patch allows WordPress to fall back to the ImageMagick command line
 when the imagic pecl is not available on the server. Patch attached.

--

Comment (by dd32):

 ''Just a note that i've removed the contents of the diff from the ticket
 description to make it easier to read.''

 Hi and welcome back to Trac.

 Just to note that there was some discussion related to the usage of the
 external commands in [comment:33:ticket:6821] of the original ticket.
 My concerns still remain from that ticket - in that I don't think
 WordPress should be shelling out to perform commands.

 Quick look at the patch points to issue in `update_size()` - not escaping
 the width/height variables, and `shell_exec()` doesn't throw exceptions.

 I think this would be great as a plugin myself.

--
Ticket URL: <https://core.trac.wordpress.org/ticket/39262#comment:2>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list