/wp-includes/class-wp-image-editor-imagick.php

https://gitlab.com/richardp2/WordPress · PHP · 494 lines · 255 code · 71 blank · 168 comment · 49 complexity · c0c7aa45b9c017d1eb2fb28d254a0398 MD5 · raw file

  1. <?php
  2. /**
  3. * WordPress Imagick Image Editor
  4. *
  5. * @package WordPress
  6. * @subpackage Image_Editor
  7. */
  8. /**
  9. * WordPress Image Editor Class for Image Manipulation through Imagick PHP Module
  10. *
  11. * @since 3.5.0
  12. * @package WordPress
  13. * @subpackage Image_Editor
  14. * @uses WP_Image_Editor Extends class
  15. */
  16. class WP_Image_Editor_Imagick extends WP_Image_Editor {
  17. protected $image = null; // Imagick Object
  18. function __destruct() {
  19. if ( $this->image instanceof Imagick ) {
  20. // we don't need the original in memory anymore
  21. $this->image->clear();
  22. $this->image->destroy();
  23. }
  24. }
  25. /**
  26. * Checks to see if current environment supports Imagick.
  27. *
  28. * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
  29. * method can be called statically.
  30. *
  31. * @since 3.5.0
  32. * @access public
  33. *
  34. * @return boolean
  35. */
  36. public static function test( $args = array() ) {
  37. // First, test Imagick's extension and classes.
  38. if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick' ) || ! class_exists( 'ImagickPixel' ) )
  39. return false;
  40. if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) )
  41. return false;
  42. $required_methods = array(
  43. 'clear',
  44. 'destroy',
  45. 'valid',
  46. 'getimage',
  47. 'writeimage',
  48. 'getimageblob',
  49. 'getimagegeometry',
  50. 'getimageformat',
  51. 'setimageformat',
  52. 'setimagecompression',
  53. 'setimagecompressionquality',
  54. 'setimagepage',
  55. 'scaleimage',
  56. 'cropimage',
  57. 'rotateimage',
  58. 'flipimage',
  59. 'flopimage',
  60. );
  61. // Now, test for deep requirements within Imagick.
  62. if ( ! defined( 'imagick::COMPRESSION_JPEG' ) )
  63. return false;
  64. if ( array_diff( $required_methods, get_class_methods( 'Imagick' ) ) )
  65. return false;
  66. return true;
  67. }
  68. /**
  69. * Checks to see if editor supports the mime-type specified.
  70. *
  71. * @since 3.5.0
  72. * @access public
  73. *
  74. * @param string $mime_type
  75. * @return boolean
  76. */
  77. public static function supports_mime_type( $mime_type ) {
  78. $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
  79. if ( ! $imagick_extension )
  80. return false;
  81. // setIteratorIndex is optional unless mime is an animated format.
  82. // Here, we just say no if you are missing it and aren't loading a jpeg.
  83. if ( ! method_exists( 'Imagick', 'setIteratorIndex' ) && $mime_type != 'image/jpeg' )
  84. return false;
  85. try {
  86. return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
  87. }
  88. catch ( Exception $e ) {
  89. return false;
  90. }
  91. }
  92. /**
  93. * Loads image from $this->file into new Imagick Object.
  94. *
  95. * @since 3.5.0
  96. * @access protected
  97. *
  98. * @return boolean|WP_Error True if loaded; WP_Error on failure.
  99. */
  100. public function load() {
  101. if ( $this->image instanceof Imagick )
  102. return true;
  103. if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) )
  104. return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
  105. /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
  106. // Even though Imagick uses less PHP memory than GD, set higher limit for users that have low PHP.ini limits
  107. @ini_set( 'memory_limit', apply_filters( 'image_memory_limit', WP_MAX_MEMORY_LIMIT ) );
  108. try {
  109. $this->image = new Imagick( $this->file );
  110. if( ! $this->image->valid() )
  111. return new WP_Error( 'invalid_image', __('File is not an image.'), $this->file);
  112. // Select the first frame to handle animated images properly
  113. if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) )
  114. $this->image->setIteratorIndex(0);
  115. $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
  116. }
  117. catch ( Exception $e ) {
  118. return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
  119. }
  120. $updated_size = $this->update_size();
  121. if ( is_wp_error( $updated_size ) )
  122. return $updated_size;
  123. return $this->set_quality( $this->quality );
  124. }
  125. /**
  126. * Sets Image Compression quality on a 1-100% scale.
  127. *
  128. * @since 3.5.0
  129. * @access public
  130. *
  131. * @param int $quality Compression Quality. Range: [1,100]
  132. * @return boolean|WP_Error True if set successfully; WP_Error on failure.
  133. */
  134. public function set_quality( $quality = null ) {
  135. $quality_result = parent::set_quality( $quality );
  136. if ( is_wp_error( $quality_result ) ) {
  137. return $quality_result;
  138. } else {
  139. $quality = $this->quality;
  140. }
  141. try {
  142. if ( 'image/jpeg' == $this->mime_type ) {
  143. $this->image->setImageCompressionQuality( $quality );
  144. $this->image->setImageCompression( imagick::COMPRESSION_JPEG );
  145. }
  146. else {
  147. $this->image->setImageCompressionQuality( $quality );
  148. }
  149. }
  150. catch ( Exception $e ) {
  151. return new WP_Error( 'image_quality_error', $e->getMessage() );
  152. }
  153. return true;
  154. }
  155. /**
  156. * Sets or updates current image size.
  157. *
  158. * @since 3.5.0
  159. * @access protected
  160. *
  161. * @param int $width
  162. * @param int $height
  163. */
  164. protected function update_size( $width = null, $height = null ) {
  165. $size = null;
  166. if ( !$width || !$height ) {
  167. try {
  168. $size = $this->image->getImageGeometry();
  169. }
  170. catch ( Exception $e ) {
  171. return new WP_Error( 'invalid_image', __('Could not read image size'), $this->file );
  172. }
  173. }
  174. if ( ! $width )
  175. $width = $size['width'];
  176. if ( ! $height )
  177. $height = $size['height'];
  178. return parent::update_size( $width, $height );
  179. }
  180. /**
  181. * Resizes current image.
  182. *
  183. * @since 3.5.0
  184. * @access public
  185. *
  186. * @param int $max_w
  187. * @param int $max_h
  188. * @param boolean $crop
  189. * @return boolean|WP_Error
  190. */
  191. public function resize( $max_w, $max_h, $crop = false ) {
  192. if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) )
  193. return true;
  194. $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
  195. if ( ! $dims )
  196. return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions') );
  197. list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
  198. if ( $crop ) {
  199. return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
  200. }
  201. try {
  202. /**
  203. * @TODO: Thumbnail is more efficient, given a newer version of Imagemagick.
  204. * $this->image->thumbnailImage( $dst_w, $dst_h );
  205. */
  206. $this->image->scaleImage( $dst_w, $dst_h );
  207. }
  208. catch ( Exception $e ) {
  209. return new WP_Error( 'image_resize_error', $e->getMessage() );
  210. }
  211. return $this->update_size( $dst_w, $dst_h );
  212. }
  213. /**
  214. * Resize multiple images from a single source.
  215. *
  216. * @since 3.5.0
  217. * @access public
  218. *
  219. * @param array $sizes {
  220. * An array of image size arrays. Default sizes are 'small', 'medium', 'large'.
  221. *
  222. * @type array $size {
  223. * @type int $width Image width.
  224. * @type int $height Image height.
  225. * @type bool $crop Optional. Whether to crop the image. Default false.
  226. * }
  227. * }
  228. * @return array An array of resized images metadata by size.
  229. */
  230. public function multi_resize( $sizes ) {
  231. $metadata = array();
  232. $orig_size = $this->size;
  233. $orig_image = $this->image->getImage();
  234. foreach ( $sizes as $size => $size_data ) {
  235. if ( ! $this->image )
  236. $this->image = $orig_image->getImage();
  237. if ( ! ( isset( $size_data['width'] ) && isset( $size_data['height'] ) ) )
  238. continue;
  239. if ( ! isset( $size_data['crop'] ) )
  240. $size_data['crop'] = false;
  241. $resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
  242. if( ! is_wp_error( $resize_result ) ) {
  243. $resized = $this->_save( $this->image );
  244. $this->image->clear();
  245. $this->image->destroy();
  246. $this->image = null;
  247. if ( ! is_wp_error( $resized ) && $resized ) {
  248. unset( $resized['path'] );
  249. $metadata[$size] = $resized;
  250. }
  251. }
  252. $this->size = $orig_size;
  253. }
  254. $this->image = $orig_image;
  255. return $metadata;
  256. }
  257. /**
  258. * Crops Image.
  259. *
  260. * @since 3.5.0
  261. * @access public
  262. *
  263. * @param string|int $src The source file or Attachment ID.
  264. * @param int $src_x The start x position to crop from.
  265. * @param int $src_y The start y position to crop from.
  266. * @param int $src_w The width to crop.
  267. * @param int $src_h The height to crop.
  268. * @param int $dst_w Optional. The destination width.
  269. * @param int $dst_h Optional. The destination height.
  270. * @param boolean $src_abs Optional. If the source crop points are absolute.
  271. * @return boolean|WP_Error
  272. */
  273. public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
  274. if ( $src_abs ) {
  275. $src_w -= $src_x;
  276. $src_h -= $src_y;
  277. }
  278. try {
  279. $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
  280. $this->image->setImagePage( $src_w, $src_h, 0, 0);
  281. if ( $dst_w || $dst_h ) {
  282. // If destination width/height isn't specified, use same as
  283. // width/height from source.
  284. if ( ! $dst_w )
  285. $dst_w = $src_w;
  286. if ( ! $dst_h )
  287. $dst_h = $src_h;
  288. $this->image->scaleImage( $dst_w, $dst_h );
  289. return $this->update_size();
  290. }
  291. }
  292. catch ( Exception $e ) {
  293. return new WP_Error( 'image_crop_error', $e->getMessage() );
  294. }
  295. return $this->update_size();
  296. }
  297. /**
  298. * Rotates current image counter-clockwise by $angle.
  299. *
  300. * @since 3.5.0
  301. * @access public
  302. *
  303. * @param float $angle
  304. * @return boolean|WP_Error
  305. */
  306. public function rotate( $angle ) {
  307. /**
  308. * $angle is 360-$angle because Imagick rotates clockwise
  309. * (GD rotates counter-clockwise)
  310. */
  311. try {
  312. $this->image->rotateImage( new ImagickPixel('none'), 360-$angle );
  313. // Since this changes the dimensions of the image, update the size.
  314. $result = $this->update_size();
  315. if ( is_wp_error( $result ) )
  316. return $result;
  317. $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
  318. }
  319. catch ( Exception $e ) {
  320. return new WP_Error( 'image_rotate_error', $e->getMessage() );
  321. }
  322. return true;
  323. }
  324. /**
  325. * Flips current image.
  326. *
  327. * @since 3.5.0
  328. * @access public
  329. *
  330. * @param boolean $horz Flip along Horizontal Axis
  331. * @param boolean $vert Flip along Vertical Axis
  332. * @returns boolean|WP_Error
  333. */
  334. public function flip( $horz, $vert ) {
  335. try {
  336. if ( $horz )
  337. $this->image->flipImage();
  338. if ( $vert )
  339. $this->image->flopImage();
  340. }
  341. catch ( Exception $e ) {
  342. return new WP_Error( 'image_flip_error', $e->getMessage() );
  343. }
  344. return true;
  345. }
  346. /**
  347. * Saves current image to file.
  348. *
  349. * @since 3.5.0
  350. * @access public
  351. *
  352. * @param string $destfilename
  353. * @param string $mime_type
  354. * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
  355. */
  356. public function save( $destfilename = null, $mime_type = null ) {
  357. $saved = $this->_save( $this->image, $destfilename, $mime_type );
  358. if ( ! is_wp_error( $saved ) ) {
  359. $this->file = $saved['path'];
  360. $this->mime_type = $saved['mime-type'];
  361. try {
  362. $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
  363. }
  364. catch ( Exception $e ) {
  365. return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
  366. }
  367. }
  368. return $saved;
  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. if ( ! $filename )
  373. $filename = $this->generate_filename( null, null, $extension );
  374. try {
  375. // Store initial Format
  376. $orig_format = $this->image->getImageFormat();
  377. $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
  378. $this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) );
  379. // Reset original Format
  380. $this->image->setImageFormat( $orig_format );
  381. }
  382. catch ( Exception $e ) {
  383. return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
  384. }
  385. // Set correct file permissions
  386. $stat = stat( dirname( $filename ) );
  387. $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
  388. @ chmod( $filename, $perms );
  389. /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
  390. return array(
  391. 'path' => $filename,
  392. 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
  393. 'width' => $this->size['width'],
  394. 'height' => $this->size['height'],
  395. 'mime-type' => $mime_type,
  396. );
  397. }
  398. /**
  399. * Streams current image to browser.
  400. *
  401. * @since 3.5.0
  402. * @access public
  403. *
  404. * @param string $mime_type
  405. * @return boolean|WP_Error
  406. */
  407. public function stream( $mime_type = null ) {
  408. list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
  409. try {
  410. // Temporarily change format for stream
  411. $this->image->setImageFormat( strtoupper( $extension ) );
  412. // Output stream of image content
  413. header( "Content-Type: $mime_type" );
  414. print $this->image->getImageBlob();
  415. // Reset Image to original Format
  416. $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
  417. }
  418. catch ( Exception $e ) {
  419. return new WP_Error( 'image_stream_error', $e->getMessage() );
  420. }
  421. return true;
  422. }
  423. }