class-wp-image-editor-imagick.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  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. *
  13. * @see WP_Image_Editor
  14. */
  15. class WP_Image_Editor_Imagick extends WP_Image_Editor {
  16. /**
  17. * Imagick object.
  18. *
  19. * @var Imagick
  20. */
  21. protected $image;
  22. public function __destruct() {
  23. if ( $this->image instanceof Imagick ) {
  24. // we don't need the original in memory anymore
  25. $this->image->clear();
  26. $this->image->destroy();
  27. }
  28. }
  29. /**
  30. * Checks to see if current environment supports Imagick.
  31. *
  32. * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
  33. * method can be called statically.
  34. *
  35. * @since 3.5.0
  36. *
  37. * @param array $args
  38. * @return bool
  39. */
  40. public static function test( $args = array() ) {
  41. // First, test Imagick's extension and classes.
  42. if ( ! extension_loaded( 'imagick' ) || ! class_exists( 'Imagick', false ) || ! class_exists( 'ImagickPixel', false ) ) {
  43. return false;
  44. }
  45. if ( version_compare( phpversion( 'imagick' ), '2.2.0', '<' ) ) {
  46. return false;
  47. }
  48. $required_methods = array(
  49. 'clear',
  50. 'destroy',
  51. 'valid',
  52. 'getimage',
  53. 'writeimage',
  54. 'getimageblob',
  55. 'getimagegeometry',
  56. 'getimageformat',
  57. 'setimageformat',
  58. 'setimagecompression',
  59. 'setimagecompressionquality',
  60. 'setimagepage',
  61. 'setoption',
  62. 'scaleimage',
  63. 'cropimage',
  64. 'rotateimage',
  65. 'flipimage',
  66. 'flopimage',
  67. 'readimage',
  68. );
  69. // Now, test for deep requirements within Imagick.
  70. if ( ! defined( 'imagick::COMPRESSION_JPEG' ) ) {
  71. return false;
  72. }
  73. $class_methods = array_map( 'strtolower', get_class_methods( 'Imagick' ) );
  74. if ( array_diff( $required_methods, $class_methods ) ) {
  75. return false;
  76. }
  77. return true;
  78. }
  79. /**
  80. * Checks to see if editor supports the mime-type specified.
  81. *
  82. * @since 3.5.0
  83. *
  84. * @param string $mime_type
  85. * @return bool
  86. */
  87. public static function supports_mime_type( $mime_type ) {
  88. $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
  89. if ( ! $imagick_extension ) {
  90. return false;
  91. }
  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. }
  97. try {
  98. // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
  99. return ( (bool) @Imagick::queryFormats( $imagick_extension ) );
  100. } catch ( Exception $e ) {
  101. return false;
  102. }
  103. }
  104. /**
  105. * Loads image from $this->file into new Imagick Object.
  106. *
  107. * @since 3.5.0
  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. }
  115. if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) ) {
  116. return new WP_Error( 'error_loading_image', __( 'File doesn&#8217;t exist?' ), $this->file );
  117. }
  118. /*
  119. * Even though Imagick uses less PHP memory than GD, set higher limit
  120. * for users that have low PHP.ini limits.
  121. */
  122. wp_raise_memory_limit( 'image' );
  123. try {
  124. $this->image = new Imagick();
  125. $file_extension = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
  126. $filename = $this->file;
  127. if ( 'pdf' === $file_extension ) {
  128. $filename = $this->pdf_setup();
  129. }
  130. // Reading image after Imagick instantiation because `setResolution`
  131. // only applies correctly before the image is read.
  132. $this->image->readImage( $filename );
  133. if ( ! $this->image->valid() ) {
  134. return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
  135. }
  136. // Select the first frame to handle animated images properly
  137. if ( is_callable( array( $this->image, 'setIteratorIndex' ) ) ) {
  138. $this->image->setIteratorIndex( 0 );
  139. }
  140. $this->mime_type = $this->get_mime_type( $this->image->getImageFormat() );
  141. } catch ( Exception $e ) {
  142. return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
  143. }
  144. $updated_size = $this->update_size();
  145. if ( is_wp_error( $updated_size ) ) {
  146. return $updated_size;
  147. }
  148. return $this->set_quality();
  149. }
  150. /**
  151. * Sets Image Compression quality on a 1-100% scale.
  152. *
  153. * @since 3.5.0
  154. *
  155. * @param int $quality Compression Quality. Range: [1,100]
  156. * @return true|WP_Error True if set successfully; WP_Error on failure.
  157. */
  158. public function set_quality( $quality = null ) {
  159. $quality_result = parent::set_quality( $quality );
  160. if ( is_wp_error( $quality_result ) ) {
  161. return $quality_result;
  162. } else {
  163. $quality = $this->get_quality();
  164. }
  165. try {
  166. if ( 'image/jpeg' === $this->mime_type ) {
  167. $this->image->setImageCompressionQuality( $quality );
  168. $this->image->setImageCompression( imagick::COMPRESSION_JPEG );
  169. } else {
  170. $this->image->setImageCompressionQuality( $quality );
  171. }
  172. } catch ( Exception $e ) {
  173. return new WP_Error( 'image_quality_error', $e->getMessage() );
  174. }
  175. return true;
  176. }
  177. /**
  178. * Sets or updates current image size.
  179. *
  180. * @since 3.5.0
  181. *
  182. * @param int $width
  183. * @param int $height
  184. *
  185. * @return true|WP_Error
  186. */
  187. protected function update_size( $width = null, $height = null ) {
  188. $size = null;
  189. if ( ! $width || ! $height ) {
  190. try {
  191. $size = $this->image->getImageGeometry();
  192. } catch ( Exception $e ) {
  193. return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
  194. }
  195. }
  196. if ( ! $width ) {
  197. $width = $size['width'];
  198. }
  199. if ( ! $height ) {
  200. $height = $size['height'];
  201. }
  202. return parent::update_size( $width, $height );
  203. }
  204. /**
  205. * Resizes current image.
  206. *
  207. * At minimum, either a height or width must be provided.
  208. * If one of the two is set to null, the resize will
  209. * maintain aspect ratio according to the provided dimension.
  210. *
  211. * @since 3.5.0
  212. *
  213. * @param int|null $max_w Image width.
  214. * @param int|null $max_h Image height.
  215. * @param bool $crop
  216. * @return bool|WP_Error
  217. */
  218. public function resize( $max_w, $max_h, $crop = false ) {
  219. if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) ) {
  220. return true;
  221. }
  222. $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
  223. if ( ! $dims ) {
  224. return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
  225. }
  226. list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
  227. if ( $crop ) {
  228. return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
  229. }
  230. // Execute the resize
  231. $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
  232. if ( is_wp_error( $thumb_result ) ) {
  233. return $thumb_result;
  234. }
  235. return $this->update_size( $dst_w, $dst_h );
  236. }
  237. /**
  238. * Efficiently resize the current image
  239. *
  240. * This is a WordPress specific implementation of Imagick::thumbnailImage(),
  241. * which resizes an image to given dimensions and removes any associated profiles.
  242. *
  243. * @since 4.5.0
  244. *
  245. * @param int $dst_w The destination width.
  246. * @param int $dst_h The destination height.
  247. * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
  248. * @param bool $strip_meta Optional. Strip all profiles, excluding color profiles, from the image. Default true.
  249. * @return bool|WP_Error
  250. */
  251. protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
  252. $allowed_filters = array(
  253. 'FILTER_POINT',
  254. 'FILTER_BOX',
  255. 'FILTER_TRIANGLE',
  256. 'FILTER_HERMITE',
  257. 'FILTER_HANNING',
  258. 'FILTER_HAMMING',
  259. 'FILTER_BLACKMAN',
  260. 'FILTER_GAUSSIAN',
  261. 'FILTER_QUADRATIC',
  262. 'FILTER_CUBIC',
  263. 'FILTER_CATROM',
  264. 'FILTER_MITCHELL',
  265. 'FILTER_LANCZOS',
  266. 'FILTER_BESSEL',
  267. 'FILTER_SINC',
  268. );
  269. /**
  270. * Set the filter value if '$filter_name' name is in our whitelist and the related
  271. * Imagick constant is defined or fall back to our default filter.
  272. */
  273. if ( in_array( $filter_name, $allowed_filters, true ) && defined( 'Imagick::' . $filter_name ) ) {
  274. $filter = constant( 'Imagick::' . $filter_name );
  275. } else {
  276. $filter = defined( 'Imagick::FILTER_TRIANGLE' ) ? Imagick::FILTER_TRIANGLE : false;
  277. }
  278. /**
  279. * Filters whether to strip metadata from images when they're resized.
  280. *
  281. * This filter only applies when resizing using the Imagick editor since GD
  282. * always strips profiles by default.
  283. *
  284. * @since 4.5.0
  285. *
  286. * @param bool $strip_meta Whether to strip image metadata during resizing. Default true.
  287. */
  288. if ( apply_filters( 'image_strip_meta', $strip_meta ) ) {
  289. $this->strip_meta(); // Fail silently if not supported.
  290. }
  291. try {
  292. /*
  293. * To be more efficient, resample large images to 5x the destination size before resizing
  294. * whenever the output size is less that 1/3 of the original image size (1/3^2 ~= .111),
  295. * unless we would be resampling to a scale smaller than 128x128.
  296. */
  297. if ( is_callable( array( $this->image, 'sampleImage' ) ) ) {
  298. $resize_ratio = ( $dst_w / $this->size['width'] ) * ( $dst_h / $this->size['height'] );
  299. $sample_factor = 5;
  300. if ( $resize_ratio < .111 && ( $dst_w * $sample_factor > 128 && $dst_h * $sample_factor > 128 ) ) {
  301. $this->image->sampleImage( $dst_w * $sample_factor, $dst_h * $sample_factor );
  302. }
  303. }
  304. /*
  305. * Use resizeImage() when it's available and a valid filter value is set.
  306. * Otherwise, fall back to the scaleImage() method for resizing, which
  307. * results in better image quality over resizeImage() with default filter
  308. * settings and retains backward compatibility with pre 4.5 functionality.
  309. */
  310. if ( is_callable( array( $this->image, 'resizeImage' ) ) && $filter ) {
  311. $this->image->setOption( 'filter:support', '2.0' );
  312. $this->image->resizeImage( $dst_w, $dst_h, $filter, 1 );
  313. } else {
  314. $this->image->scaleImage( $dst_w, $dst_h );
  315. }
  316. // Set appropriate quality settings after resizing.
  317. if ( 'image/jpeg' === $this->mime_type ) {
  318. if ( is_callable( array( $this->image, 'unsharpMaskImage' ) ) ) {
  319. $this->image->unsharpMaskImage( 0.25, 0.25, 8, 0.065 );
  320. }
  321. $this->image->setOption( 'jpeg:fancy-upsampling', 'off' );
  322. }
  323. if ( 'image/png' === $this->mime_type ) {
  324. $this->image->setOption( 'png:compression-filter', '5' );
  325. $this->image->setOption( 'png:compression-level', '9' );
  326. $this->image->setOption( 'png:compression-strategy', '1' );
  327. $this->image->setOption( 'png:exclude-chunk', 'all' );
  328. }
  329. /*
  330. * If alpha channel is not defined, set it opaque.
  331. *
  332. * Note that Imagick::getImageAlphaChannel() is only available if Imagick
  333. * has been compiled against ImageMagick version 6.4.0 or newer.
  334. */
  335. if ( is_callable( array( $this->image, 'getImageAlphaChannel' ) )
  336. && is_callable( array( $this->image, 'setImageAlphaChannel' ) )
  337. && defined( 'Imagick::ALPHACHANNEL_UNDEFINED' )
  338. && defined( 'Imagick::ALPHACHANNEL_OPAQUE' )
  339. ) {
  340. if ( $this->image->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED ) {
  341. $this->image->setImageAlphaChannel( Imagick::ALPHACHANNEL_OPAQUE );
  342. }
  343. }
  344. // Limit the bit depth of resized images to 8 bits per channel.
  345. if ( is_callable( array( $this->image, 'getImageDepth' ) ) && is_callable( array( $this->image, 'setImageDepth' ) ) ) {
  346. if ( 8 < $this->image->getImageDepth() ) {
  347. $this->image->setImageDepth( 8 );
  348. }
  349. }
  350. if ( is_callable( array( $this->image, 'setInterlaceScheme' ) ) && defined( 'Imagick::INTERLACE_NO' ) ) {
  351. $this->image->setInterlaceScheme( Imagick::INTERLACE_NO );
  352. }
  353. } catch ( Exception $e ) {
  354. return new WP_Error( 'image_resize_error', $e->getMessage() );
  355. }
  356. }
  357. /**
  358. * Create multiple smaller images from a single source.
  359. *
  360. * Attempts to create all sub-sizes and returns the meta data at the end. This
  361. * may result in the server running out of resources. When it fails there may be few
  362. * "orphaned" images left over as the meta data is never returned and saved.
  363. *
  364. * As of 5.3.0 the preferred way to do this is with `make_subsize()`. It creates
  365. * the new images one at a time and allows for the meta data to be saved after
  366. * each new image is created.
  367. *
  368. * @since 3.5.0
  369. *
  370. * @param array $sizes {
  371. * An array of image size data arrays.
  372. *
  373. * Either a height or width must be provided.
  374. * If one of the two is set to null, the resize will
  375. * maintain aspect ratio according to the provided dimension.
  376. *
  377. * @type array $size {
  378. * Array of height, width values, and whether to crop.
  379. *
  380. * @type int $width Image width. Optional if `$height` is specified.
  381. * @type int $height Image height. Optional if `$width` is specified.
  382. * @type bool $crop Optional. Whether to crop the image. Default false.
  383. * }
  384. * }
  385. * @return array An array of resized images' metadata by size.
  386. */
  387. public function multi_resize( $sizes ) {
  388. $metadata = array();
  389. foreach ( $sizes as $size => $size_data ) {
  390. $meta = $this->make_subsize( $size_data );
  391. if ( ! is_wp_error( $meta ) ) {
  392. $metadata[ $size ] = $meta;
  393. }
  394. }
  395. return $metadata;
  396. }
  397. /**
  398. * Create an image sub-size and return the image meta data value for it.
  399. *
  400. * @since 5.3.0
  401. *
  402. * @param array $size_data Array of width, height, and whether to crop.
  403. * @return WP_Error|array WP_Error on error, or the image data array for inclusion in the `sizes` array in the image meta.
  404. */
  405. public function make_subsize( $size_data ) {
  406. if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
  407. return new WP_Error( 'image_subsize_create_error', __( 'Cannot resize the image. Both width and height are not set.' ) );
  408. }
  409. $orig_size = $this->size;
  410. $orig_image = $this->image->getImage();
  411. if ( ! isset( $size_data['width'] ) ) {
  412. $size_data['width'] = null;
  413. }
  414. if ( ! isset( $size_data['height'] ) ) {
  415. $size_data['height'] = null;
  416. }
  417. if ( ! isset( $size_data['crop'] ) ) {
  418. $size_data['crop'] = false;
  419. }
  420. $resized = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
  421. if ( is_wp_error( $resized ) ) {
  422. $saved = $resized;
  423. } else {
  424. $saved = $this->_save( $this->image );
  425. $this->image->clear();
  426. $this->image->destroy();
  427. $this->image = null;
  428. }
  429. $this->size = $orig_size;
  430. $this->image = $orig_image;
  431. if ( ! is_wp_error( $saved ) ) {
  432. unset( $saved['path'] );
  433. }
  434. return $saved;
  435. }
  436. /**
  437. * Crops Image.
  438. *
  439. * @since 3.5.0
  440. *
  441. * @param int $src_x The start x position to crop from.
  442. * @param int $src_y The start y position to crop from.
  443. * @param int $src_w The width to crop.
  444. * @param int $src_h The height to crop.
  445. * @param int $dst_w Optional. The destination width.
  446. * @param int $dst_h Optional. The destination height.
  447. * @param bool $src_abs Optional. If the source crop points are absolute.
  448. * @return bool|WP_Error
  449. */
  450. public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
  451. if ( $src_abs ) {
  452. $src_w -= $src_x;
  453. $src_h -= $src_y;
  454. }
  455. try {
  456. $this->image->cropImage( $src_w, $src_h, $src_x, $src_y );
  457. $this->image->setImagePage( $src_w, $src_h, 0, 0 );
  458. if ( $dst_w || $dst_h ) {
  459. // If destination width/height isn't specified, use same as
  460. // width/height from source.
  461. if ( ! $dst_w ) {
  462. $dst_w = $src_w;
  463. }
  464. if ( ! $dst_h ) {
  465. $dst_h = $src_h;
  466. }
  467. $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
  468. if ( is_wp_error( $thumb_result ) ) {
  469. return $thumb_result;
  470. }
  471. return $this->update_size();
  472. }
  473. } catch ( Exception $e ) {
  474. return new WP_Error( 'image_crop_error', $e->getMessage() );
  475. }
  476. return $this->update_size();
  477. }
  478. /**
  479. * Rotates current image counter-clockwise by $angle.
  480. *
  481. * @since 3.5.0
  482. *
  483. * @param float $angle
  484. * @return true|WP_Error
  485. */
  486. public function rotate( $angle ) {
  487. /**
  488. * $angle is 360-$angle because Imagick rotates clockwise
  489. * (GD rotates counter-clockwise)
  490. */
  491. try {
  492. $this->image->rotateImage( new ImagickPixel( 'none' ), 360 - $angle );
  493. // Normalise EXIF orientation data so that display is consistent across devices.
  494. if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
  495. $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
  496. }
  497. // Since this changes the dimensions of the image, update the size.
  498. $result = $this->update_size();
  499. if ( is_wp_error( $result ) ) {
  500. return $result;
  501. }
  502. $this->image->setImagePage( $this->size['width'], $this->size['height'], 0, 0 );
  503. } catch ( Exception $e ) {
  504. return new WP_Error( 'image_rotate_error', $e->getMessage() );
  505. }
  506. return true;
  507. }
  508. /**
  509. * Flips current image.
  510. *
  511. * @since 3.5.0
  512. *
  513. * @param bool $horz Flip along Horizontal Axis
  514. * @param bool $vert Flip along Vertical Axis
  515. * @return true|WP_Error
  516. */
  517. public function flip( $horz, $vert ) {
  518. try {
  519. if ( $horz ) {
  520. $this->image->flipImage();
  521. }
  522. if ( $vert ) {
  523. $this->image->flopImage();
  524. }
  525. // Normalise EXIF orientation data so that display is consistent across devices.
  526. if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
  527. $this->image->setImageOrientation( Imagick::ORIENTATION_TOPLEFT );
  528. }
  529. } catch ( Exception $e ) {
  530. return new WP_Error( 'image_flip_error', $e->getMessage() );
  531. }
  532. return true;
  533. }
  534. /**
  535. * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
  536. *
  537. * As ImageMagick copies the EXIF data to the flipped/rotated image, proceed only
  538. * if EXIF Orientation can be reset afterwards.
  539. *
  540. * @since 5.3.0
  541. *
  542. * @return bool|WP_Error True if the image was rotated. False if no EXIF data or if the image doesn't need rotation.
  543. * WP_Error if error while rotating.
  544. */
  545. public function maybe_exif_rotate() {
  546. if ( is_callable( array( $this->image, 'setImageOrientation' ) ) && defined( 'Imagick::ORIENTATION_TOPLEFT' ) ) {
  547. return parent::maybe_exif_rotate();
  548. } else {
  549. return new WP_Error( 'write_exif_error', __( 'The image cannot be rotated because the embedded meta data cannot be updated.' ) );
  550. }
  551. }
  552. /**
  553. * Saves current image to file.
  554. *
  555. * @since 3.5.0
  556. *
  557. * @param string $destfilename
  558. * @param string $mime_type
  559. * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
  560. */
  561. public function save( $destfilename = null, $mime_type = null ) {
  562. $saved = $this->_save( $this->image, $destfilename, $mime_type );
  563. if ( ! is_wp_error( $saved ) ) {
  564. $this->file = $saved['path'];
  565. $this->mime_type = $saved['mime-type'];
  566. try {
  567. $this->image->setImageFormat( strtoupper( $this->get_extension( $this->mime_type ) ) );
  568. } catch ( Exception $e ) {
  569. return new WP_Error( 'image_save_error', $e->getMessage(), $this->file );
  570. }
  571. }
  572. return $saved;
  573. }
  574. /**
  575. * @param Imagick $image
  576. * @param string $filename
  577. * @param string $mime_type
  578. * @return array|WP_Error
  579. */
  580. protected function _save( $image, $filename = null, $mime_type = null ) {
  581. list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
  582. if ( ! $filename ) {
  583. $filename = $this->generate_filename( null, null, $extension );
  584. }
  585. try {
  586. // Store initial Format
  587. $orig_format = $this->image->getImageFormat();
  588. $this->image->setImageFormat( strtoupper( $this->get_extension( $mime_type ) ) );
  589. $this->make_image( $filename, array( $image, 'writeImage' ), array( $filename ) );
  590. // Reset original Format
  591. $this->image->setImageFormat( $orig_format );
  592. } catch ( Exception $e ) {
  593. return new WP_Error( 'image_save_error', $e->getMessage(), $filename );
  594. }
  595. // Set correct file permissions
  596. $stat = stat( dirname( $filename ) );
  597. $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
  598. chmod( $filename, $perms );
  599. return array(
  600. 'path' => $filename,
  601. /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
  602. 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
  603. 'width' => $this->size['width'],
  604. 'height' => $this->size['height'],
  605. 'mime-type' => $mime_type,
  606. );
  607. }
  608. /**
  609. * Streams current image to browser.
  610. *
  611. * @since 3.5.0
  612. *
  613. * @param string $mime_type The mime type of the image.
  614. * @return bool|WP_Error True on success, WP_Error object on failure.
  615. */
  616. public function stream( $mime_type = null ) {
  617. list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
  618. try {
  619. // Temporarily change format for stream
  620. $this->image->setImageFormat( strtoupper( $extension ) );
  621. // Output stream of image content
  622. header( "Content-Type: $mime_type" );
  623. print $this->image->getImageBlob();
  624. // Reset Image to original Format
  625. $this->image->setImageFormat( $this->get_extension( $this->mime_type ) );
  626. } catch ( Exception $e ) {
  627. return new WP_Error( 'image_stream_error', $e->getMessage() );
  628. }
  629. return true;
  630. }
  631. /**
  632. * Strips all image meta except color profiles from an image.
  633. *
  634. * @since 4.5.0
  635. *
  636. * @return true|WP_Error True if stripping metadata was successful. WP_Error object on error.
  637. */
  638. protected function strip_meta() {
  639. if ( ! is_callable( array( $this->image, 'getImageProfiles' ) ) ) {
  640. /* translators: %s: ImageMagick method name. */
  641. return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::getImageProfiles()</code>' ) );
  642. }
  643. if ( ! is_callable( array( $this->image, 'removeImageProfile' ) ) ) {
  644. /* translators: %s: ImageMagick method name. */
  645. return new WP_Error( 'image_strip_meta_error', sprintf( __( '%s is required to strip image meta.' ), '<code>Imagick::removeImageProfile()</code>' ) );
  646. }
  647. /*
  648. * Protect a few profiles from being stripped for the following reasons:
  649. *
  650. * - icc: Color profile information
  651. * - icm: Color profile information
  652. * - iptc: Copyright data
  653. * - exif: Orientation data
  654. * - xmp: Rights usage data
  655. */
  656. $protected_profiles = array(
  657. 'icc',
  658. 'icm',
  659. 'iptc',
  660. 'exif',
  661. 'xmp',
  662. );
  663. try {
  664. // Strip profiles.
  665. foreach ( $this->image->getImageProfiles( '*', true ) as $key => $value ) {
  666. if ( ! in_array( $key, $protected_profiles, true ) ) {
  667. $this->image->removeImageProfile( $key );
  668. }
  669. }
  670. } catch ( Exception $e ) {
  671. return new WP_Error( 'image_strip_meta_error', $e->getMessage() );
  672. }
  673. return true;
  674. }
  675. /**
  676. * Sets up Imagick for PDF processing.
  677. * Increases rendering DPI and only loads first page.
  678. *
  679. * @since 4.7.0
  680. *
  681. * @return string|WP_Error File to load or WP_Error on failure.
  682. */
  683. protected function pdf_setup() {
  684. try {
  685. // By default, PDFs are rendered in a very low resolution.
  686. // We want the thumbnail to be readable, so increase the rendering DPI.
  687. $this->image->setResolution( 128, 128 );
  688. // When generating thumbnails from cropped PDF pages, Imagemagick uses the uncropped
  689. // area (resulting in unnecessary whitespace) unless the following option is set.
  690. $this->image->setOption( 'pdf:use-cropbox', true );
  691. // Only load the first page.
  692. return $this->file . '[0]';
  693. } catch ( Exception $e ) {
  694. return new WP_Error( 'pdf_setup_failed', $e->getMessage(), $this->file );
  695. }
  696. }
  697. }