WordPress.org

Make WordPress Core

Ticket #39262: external-imagic.diff

File external-imagic.diff, 12.9 KB (added by Hristo Sg, 5 years ago)

The patch for falling back to command line

  • new file wp-includes/class-wp-image-editor-imagick-external.php

    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
    - +  
     1<?php
     2/**
     3 * WordPress Imagick Image Editor
     4 *
     5 * @package WordPress
     6 * @subpackage Image_Editor
     7 */
     8
     9/**
     10 * WordPress Image Editor Class for Image Manipulation through Imagick command line utilities
     11 *
     12 * @since 3.5.0
     13 * @package WordPress
     14 * @subpackage Image_Editor
     15 * @uses WP_Image_Editor Extends class
     16 */
     17class WP_Image_Editor_Imagick_External extends WP_Image_Editor {
     18        /**
     19         * Imagick object.
     20         *
     21         * @access protected
     22         * @var Imagick
     23         */
     24        protected $image;
     25        public static $prog_convert  = '/usr/bin/convert';
     26        public static $prog_identify = '/usr/bin/identify';
     27
     28        public function __destruct() {
     29        }
     30
     31        /**
     32         * Checks to see if current environment supports Imagick.
     33         *
     34         * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
     35         * method can be called statically.
     36         *
     37         * @since 3.5.0
     38         *
     39         * @static
     40         * @access public
     41         *
     42         * @param array $args
     43         * @return bool
     44         */
     45        public static function test( $args = array() ) {
     46                return is_executable( self::$prog_convert ) && is_executable( self::$prog_identify );
     47        }
     48
     49        /**
     50         * Checks to see if editor supports the mime-type specified.
     51         *
     52         * @since 3.5.0
     53         *
     54         * @static
     55         * @access public
     56         *
     57         * @param string $mime_type
     58         * @return bool
     59         */
     60        public static function supports_mime_type( $mime_type ) {
     61                $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
     62
     63                if ( ! $imagick_extension )
     64                        return false;
     65
     66                if ( $imagick_extension === 'PDF' )
     67                        return true;
     68
     69                return false;
     70        }
     71
     72        /**
     73         * Loads image from $this->file into new Imagick Object.
     74         *
     75         * @since 3.5.0
     76         * @access protected
     77         *
     78         * @return true|WP_Error True if loaded; WP_Error on failure.
     79         */
     80        public function load() {
     81                if ( $this->image )
     82                        return true;
     83
     84                $this->image = $this->file;
     85
     86                if ( ! is_file( $this->file ) )
     87                        return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
     88
     89                try {
     90                        $filename = $this->file;
     91
     92                        list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename );
     93                        $this->mime_type = $mime_type;
     94                }
     95                catch ( Exception $e ) {
     96                        return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
     97                }
     98
     99                $updated_size = $this->update_size();
     100                if ( is_wp_error( $updated_size ) ) {
     101                        return $updated_size;
     102                }
     103
     104                return $this->set_quality();
     105        }
     106
     107        /**
     108         * Sets Image Compression quality on a 1-100% scale.
     109         *
     110         * @since 3.5.0
     111         * @access public
     112         *
     113         * @param int $quality Compression Quality. Range: [1,100]
     114         * @return true|WP_Error True if set successfully; WP_Error on failure.
     115         */
     116        public function set_quality( $quality = null ) {
     117                $quality_result = parent::set_quality( $quality );
     118                if ( is_wp_error( $quality_result ) ) {
     119                        return $quality_result;
     120                } else {
     121                        $quality = $this->get_quality();
     122                }
     123                return true;
     124        }
     125
     126        /**
     127         * Sets or updates current image size.
     128         *
     129         * @since 3.5.0
     130         * @access protected
     131         *
     132         * @param int $width
     133         * @param int $height
     134         *
     135         * @return true|WP_Error
     136         */
     137        protected function update_size( $width = null, $height = null ) {
     138                $size = null;
     139                if ( !$width || !$height ) {
     140                        try {
     141                                $ret = shell_exec( self::$prog_identify . " -format '%[width] %[height]' " .  escapeshellarg( $this->file ) . " 2>/dev/null" );
     142                                list( $width, $height ) = explode( " ", trim( $ret ) );
     143                        }
     144                        catch ( Exception $e ) {
     145                                return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
     146                        }
     147                }
     148                if ( ! $width )
     149                        $width = $size['width'];
     150                if ( ! $height )
     151                        $height = $size['height'];
     152
     153                return parent::update_size( $width, $height );
     154        }
     155
     156        /**
     157         * Resizes current image.
     158         *
     159         * At minimum, either a height or width must be provided.
     160         * If one of the two is set to null, the resize will
     161         * maintain aspect ratio according to the provided dimension.
     162         *
     163         * @since 3.5.0
     164         * @access public
     165         *
     166         * @param  int|null $max_w Image width.
     167         * @param  int|null $max_h Image height.
     168         * @param  bool     $crop
     169         * @return bool|WP_Error
     170         */
     171        public function resize( $max_w, $max_h, $crop = false ) {
     172                if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) )
     173                        return true;
     174
     175                $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
     176                if ( ! $dims )
     177                        return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions') );
     178                list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
     179
     180                if ( $crop ) {
     181                        return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
     182                }
     183
     184                // Execute the resize
     185                $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
     186                if ( is_wp_error( $thumb_result ) ) {
     187                        return $thumb_result;
     188                }
     189
     190                return $this->update_size( $dst_w, $dst_h );
     191        }
     192
     193        /**
     194         * Efficiently resize the current image
     195         *
     196         * This is a WordPress specific implementation of Imagick::thumbnailImage(),
     197         * which resizes an image to given dimensions and removes any associated profiles.
     198         *
     199         * @since 4.5.0
     200         * @access protected
     201         *
     202         * @param int    $dst_w       The destination width.
     203         * @param int    $dst_h       The destination height.
     204         * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
     205         * @param bool   $strip_meta  Optional. Strip all profiles, excluding color profiles, from the image. Default true.
     206         * @return bool|WP_Error
     207         */
     208        protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
     209                list( $filename, $extension, $mime_type ) = $this->get_output_format( $this->filename, $this->mime_type );
     210                $dst_w = intval( $dst_w );
     211                $dst_h = intval( $dst_h );
     212                $filename = $this->generate_filename( "${dst_w}x${dst_h}", null, $extension );
     213
     214                $ret = 0;
     215                $cmd = self::$prog_convert .
     216                        " " . escapeshellarg( $this->file ) .
     217                        " -resize " . escapeshellarg( "${dst_w}x$dst_h" ) .
     218                        " -quality " . escapeshellarg( $this->quality ).
     219                        " " . escapeshellarg( $filename );
     220
     221                system( $cmd, $ret );
     222                if ( $ret !== 0 )
     223                        return new WP_Error( 'image_resize_error', "convert returned error: $ret", $filename );
     224
     225                return true;
     226        }
     227
     228        /**
     229         * Resize multiple images from a single source.
     230         *
     231         * @since 3.5.0
     232         * @access public
     233         *
     234         * @param array $sizes {
     235         *     An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'.
     236         *
     237         *     Either a height or width must be provided.
     238         *     If one of the two is set to null, the resize will
     239         *     maintain aspect ratio according to the provided dimension.
     240         *
     241         *     @type array $size {
     242         *         Array of height, width values, and whether to crop.
     243         *
     244         *         @type int  $width  Image width. Optional if `$height` is specified.
     245         *         @type int  $height Image height. Optional if `$width` is specified.
     246         *         @type bool $crop   Optional. Whether to crop the image. Default false.
     247         *     }
     248         * }
     249         * @return array An array of resized images' metadata by size.
     250         */
     251        public function multi_resize( $sizes ) {
     252                $metadata = array();
     253                $orig_size = $this->size;
     254                $orig_image = $this->image;
     255
     256                foreach ( $sizes as $size => $size_data ) {
     257                        if ( ! $this->image )
     258                                $this->image = $orig_image;
     259
     260                        if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
     261                                continue;
     262                        }
     263
     264                        if ( ! isset( $size_data['width'] ) ) {
     265                                $size_data['width'] = null;
     266                        }
     267                        if ( ! isset( $size_data['height'] ) ) {
     268                                $size_data['height'] = null;
     269                        }
     270
     271                        if ( ! isset( $size_data['crop'] ) ) {
     272                                $size_data['crop'] = false;
     273                        }
     274
     275                        $resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
     276
     277                        $duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
     278
     279                        if ( ! is_wp_error( $resize_result ) && ! $duplicate ) {
     280                                $resized = $this->_save( $this->image );
     281                                $this->image = null;
     282                                if ( ! is_wp_error( $resized ) && $resized ) {
     283                                        unset( $resized['path'] );
     284                                        $metadata[$size] = $resized;
     285                                }
     286                        }
     287
     288                        $this->size = $orig_size;
     289                }
     290
     291                $this->image = $orig_image;
     292
     293                return $metadata;
     294        }
     295
     296        /**
     297         * Crops Image.
     298         *
     299         * @since 3.5.0
     300         * @access public
     301         *
     302         * @param int  $src_x The start x position to crop from.
     303         * @param int  $src_y The start y position to crop from.
     304         * @param int  $src_w The width to crop.
     305         * @param int  $src_h The height to crop.
     306         * @param int  $dst_w Optional. The destination width.
     307         * @param int  $dst_h Optional. The destination height.
     308         * @param bool $src_abs Optional. If the source crop points are absolute.
     309         * @return bool|WP_Error
     310         */
     311        public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
     312                return new WP_Error( 'image_crop_error', 'Unsupported operation' );
     313        }
     314
     315        /**
     316         * Rotates current image counter-clockwise by $angle.
     317         *
     318         * @since 3.5.0
     319         * @access public
     320         *
     321         * @param float $angle
     322         * @return true|WP_Error
     323         */
     324        public function rotate( $angle ) {
     325                return new WP_Error( 'image_rotate_error', 'Unsupported operation' );
     326        }
     327
     328        /**
     329         * Flips current image.
     330         *
     331         * @since 3.5.0
     332         * @access public
     333         *
     334         * @param bool $horz Flip along Horizontal Axis
     335         * @param bool $vert Flip along Vertical Axis
     336         * @return true|WP_Error
     337         */
     338        public function flip( $horz, $vert ) {
     339                return new WP_Error( 'image_flip_error', 'Unsupported operation' );
     340        }
     341
     342        /**
     343         * Saves current image to file.
     344         *
     345         * @since 3.5.0
     346         * @access public
     347         *
     348         * @param string $destfilename
     349         * @param string $mime_type
     350         * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
     351         */
     352        public function save( $destfilename = null, $mime_type = null ) {
     353                $saved = $this->_save( $this->image, $destfilename, $mime_type );
     354
     355                if ( ! is_wp_error( $saved ) ) {
     356                        $this->file = $saved['path'];
     357                        $this->mime_type = $saved['mime-type'];
     358                }
     359
     360                return $saved;
     361        }
     362
     363        /**
     364         *
     365         * @param Imagick $image
     366         * @param string $filename
     367         * @param string $mime_type
     368         * @return array|WP_Error
     369         */
     370        protected function _save( $image, $filename = null, $mime_type = null ) {
     371                list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
     372
     373                if ( ! $filename )
     374                        $filename = $this->generate_filename( null, null, $extension );
     375
     376                $ret = 0;
     377                $cmd = self::$prog_convert .
     378                        " " . escapeshellarg( $this->file ) .
     379                        " -quality " . escapeshellarg($this->quality) .
     380                        " +repage" .
     381                        " " . escapeshellarg( $filename );
     382                system( $cmd, $ret );
     383                if ( $ret !== 0 )
     384                        return new WP_Error( 'image_save_error', "convert returned error: $ret", $filename );
     385
     386                // Set correct file permissions
     387                $stat = stat( dirname( $filename ) );
     388                $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
     389                @ chmod( $filename, $perms );
     390
     391                /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
     392                return array(
     393                        'path'      => $filename,
     394                        'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
     395                        'width'     => $this->size['width'],
     396                        'height'    => $this->size['height'],
     397                        'mime-type' => $mime_type,
     398                );
     399        }
     400
     401        public function stream( $mime_type = null ) {
     402                header( "Content-Type: $mime_type" );
     403                readfile( $this->file );
     404                return;
     405        }
     406
     407}
  • wp-includes/media.php

    diff --git a/wp-includes/media.php b/wp-includes/media.php
    index ba52555..a7aa3ad 100644
    a b function _wp_image_editor_choose( $args = array() ) { 
    29282928        require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
    29292929        require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
    29302930        require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
     2931        require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick-external.php';
    29312932        /**
    29322933         * Filters the list of image editing library classes.
    29332934         *
    29342935         * @since 3.5.0
    29352936         *
    29362937         * @param array $image_editors List of available image editors. Defaults are
    2937          *                             'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD'.
     2938         *                             'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD', 'WP_Image_Editor_Imagick_External'
    29382939         */
    2939         $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) );
     2940        $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD',
     2941                'WP_Image_Editor_Imagick_External' ) );
    29402942
    29412943        foreach ( $implementations as $implementation ) {
    29422944                if ( ! call_user_func( array( $implementation, 'test' ), $args ) )