[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
Tue Dec 13 11:59:25 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              |   Keywords:
  Focuses:                      |
--------------------------------+-----------------------------
 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 ) )

 }}}

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


More information about the wp-trac mailing list