[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