PageRenderTime 49ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

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

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