You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

482 lines
13 KiB

  1. <?php
  2. /**
  3. * WordPress GD Image Editor
  4. *
  5. * @package WordPress
  6. * @subpackage Image_Editor
  7. */
  8. /**
  9. * WordPress Image Editor Class for Image Manipulation through GD
  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_GD extends WP_Image_Editor {
  17. /**
  18. * GD Resource.
  19. *
  20. * @access protected
  21. * @var resource
  22. */
  23. protected $image;
  24. public function __destruct() {
  25. if ( $this->image ) {
  26. // we don't need the original in memory anymore
  27. imagedestroy( $this->image );
  28. }
  29. }
  30. /**
  31. * Checks to see if current environment supports GD.
  32. *
  33. * @since 3.5.0
  34. *
  35. * @static
  36. * @access public
  37. *
  38. * @param array $args
  39. * @return bool
  40. */
  41. public static function test( $args = array() ) {
  42. if ( ! extension_loaded('gd') || ! function_exists('gd_info') )
  43. return false;
  44. // On some setups GD library does not provide imagerotate() - Ticket #11536
  45. if ( isset( $args['methods'] ) &&
  46. in_array( 'rotate', $args['methods'] ) &&
  47. ! function_exists('imagerotate') ){
  48. return false;
  49. }
  50. return true;
  51. }
  52. /**
  53. * Checks to see if editor supports the mime-type specified.
  54. *
  55. * @since 3.5.0
  56. *
  57. * @static
  58. * @access public
  59. *
  60. * @param string $mime_type
  61. * @return bool
  62. */
  63. public static function supports_mime_type( $mime_type ) {
  64. $image_types = imagetypes();
  65. switch( $mime_type ) {
  66. case 'image/jpeg':
  67. return ($image_types & IMG_JPG) != 0;
  68. case 'image/png':
  69. return ($image_types & IMG_PNG) != 0;
  70. case 'image/gif':
  71. return ($image_types & IMG_GIF) != 0;
  72. }
  73. return false;
  74. }
  75. /**
  76. * Loads image from $this->file into new GD Resource.
  77. *
  78. * @since 3.5.0
  79. * @access protected
  80. *
  81. * @return bool|WP_Error True if loaded successfully; WP_Error on failure.
  82. */
  83. public function load() {
  84. if ( $this->image )
  85. return true;
  86. if ( ! is_file( $this->file ) && ! preg_match( '|^https?://|', $this->file ) )
  87. return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
  88. // Set artificially high because GD uses uncompressed images in memory.
  89. wp_raise_memory_limit( 'image' );
  90. $this->image = @imagecreatefromstring( file_get_contents( $this->file ) );
  91. if ( ! is_resource( $this->image ) )
  92. return new WP_Error( 'invalid_image', __('File is not an image.'), $this->file );
  93. $size = @getimagesize( $this->file );
  94. if ( ! $size )
  95. return new WP_Error( 'invalid_image', __('Could not read image size.'), $this->file );
  96. if ( function_exists( 'imagealphablending' ) && function_exists( 'imagesavealpha' ) ) {
  97. imagealphablending( $this->image, false );
  98. imagesavealpha( $this->image, true );
  99. }
  100. $this->update_size( $size[0], $size[1] );
  101. $this->mime_type = $size['mime'];
  102. return $this->set_quality();
  103. }
  104. /**
  105. * Sets or updates current image size.
  106. *
  107. * @since 3.5.0
  108. * @access protected
  109. *
  110. * @param int $width
  111. * @param int $height
  112. * @return true
  113. */
  114. protected function update_size( $width = false, $height = false ) {
  115. if ( ! $width )
  116. $width = imagesx( $this->image );
  117. if ( ! $height )
  118. $height = imagesy( $this->image );
  119. return parent::update_size( $width, $height );
  120. }
  121. /**
  122. * Resizes current image.
  123. * Wraps _resize, since _resize returns a GD Resource.
  124. *
  125. * At minimum, either a height or width must be provided.
  126. * If one of the two is set to null, the resize will
  127. * maintain aspect ratio according to the provided dimension.
  128. *
  129. * @since 3.5.0
  130. * @access public
  131. *
  132. * @param int|null $max_w Image width.
  133. * @param int|null $max_h Image height.
  134. * @param bool $crop
  135. * @return true|WP_Error
  136. */
  137. public function resize( $max_w, $max_h, $crop = false ) {
  138. if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) )
  139. return true;
  140. $resized = $this->_resize( $max_w, $max_h, $crop );
  141. if ( is_resource( $resized ) ) {
  142. imagedestroy( $this->image );
  143. $this->image = $resized;
  144. return true;
  145. } elseif ( is_wp_error( $resized ) )
  146. return $resized;
  147. return new WP_Error( 'image_resize_error', __('Image resize failed.'), $this->file );
  148. }
  149. /**
  150. *
  151. * @param int $max_w
  152. * @param int $max_h
  153. * @param bool|array $crop
  154. * @return resource|WP_Error
  155. */
  156. protected function _resize( $max_w, $max_h, $crop = false ) {
  157. $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
  158. if ( ! $dims ) {
  159. return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions'), $this->file );
  160. }
  161. list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
  162. $resized = wp_imagecreatetruecolor( $dst_w, $dst_h );
  163. imagecopyresampled( $resized, $this->image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h );
  164. if ( is_resource( $resized ) ) {
  165. $this->update_size( $dst_w, $dst_h );
  166. return $resized;
  167. }
  168. return new WP_Error( 'image_resize_error', __('Image resize failed.'), $this->file );
  169. }
  170. /**
  171. * Resize multiple images from a single source.
  172. *
  173. * @since 3.5.0
  174. * @access public
  175. *
  176. * @param array $sizes {
  177. * An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'.
  178. *
  179. * Either a height or width must be provided.
  180. * If one of the two is set to null, the resize will
  181. * maintain aspect ratio according to the provided dimension.
  182. *
  183. * @type array $size {
  184. * Array of height, width values, and whether to crop.
  185. *
  186. * @type int $width Image width. Optional if `$height` is specified.
  187. * @type int $height Image height. Optional if `$width` is specified.
  188. * @type bool $crop Optional. Whether to crop the image. Default false.
  189. * }
  190. * }
  191. * @return array An array of resized images' metadata by size.
  192. */
  193. public function multi_resize( $sizes ) {
  194. $metadata = array();
  195. $orig_size = $this->size;
  196. foreach ( $sizes as $size => $size_data ) {
  197. if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
  198. continue;
  199. }
  200. if ( ! isset( $size_data['width'] ) ) {
  201. $size_data['width'] = null;
  202. }
  203. if ( ! isset( $size_data['height'] ) ) {
  204. $size_data['height'] = null;
  205. }
  206. if ( ! isset( $size_data['crop'] ) ) {
  207. $size_data['crop'] = false;
  208. }
  209. $image = $this->_resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
  210. $duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
  211. if ( ! is_wp_error( $image ) && ! $duplicate ) {
  212. $resized = $this->_save( $image );
  213. imagedestroy( $image );
  214. if ( ! is_wp_error( $resized ) && $resized ) {
  215. unset( $resized['path'] );
  216. $metadata[$size] = $resized;
  217. }
  218. }
  219. $this->size = $orig_size;
  220. }
  221. return $metadata;
  222. }
  223. /**
  224. * Crops Image.
  225. *
  226. * @since 3.5.0
  227. * @access public
  228. *
  229. * @param int $src_x The start x position to crop from.
  230. * @param int $src_y The start y position to crop from.
  231. * @param int $src_w The width to crop.
  232. * @param int $src_h The height to crop.
  233. * @param int $dst_w Optional. The destination width.
  234. * @param int $dst_h Optional. The destination height.
  235. * @param bool $src_abs Optional. If the source crop points are absolute.
  236. * @return bool|WP_Error
  237. */
  238. public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
  239. // If destination width/height isn't specified, use same as
  240. // width/height from source.
  241. if ( ! $dst_w )
  242. $dst_w = $src_w;
  243. if ( ! $dst_h )
  244. $dst_h = $src_h;
  245. $dst = wp_imagecreatetruecolor( $dst_w, $dst_h );
  246. if ( $src_abs ) {
  247. $src_w -= $src_x;
  248. $src_h -= $src_y;
  249. }
  250. if ( function_exists( 'imageantialias' ) )
  251. imageantialias( $dst, true );
  252. imagecopyresampled( $dst, $this->image, 0, 0, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h );
  253. if ( is_resource( $dst ) ) {
  254. imagedestroy( $this->image );
  255. $this->image = $dst;
  256. $this->update_size();
  257. return true;
  258. }
  259. return new WP_Error( 'image_crop_error', __('Image crop failed.'), $this->file );
  260. }
  261. /**
  262. * Rotates current image counter-clockwise by $angle.
  263. * Ported from image-edit.php
  264. *
  265. * @since 3.5.0
  266. * @access public
  267. *
  268. * @param float $angle
  269. * @return true|WP_Error
  270. */
  271. public function rotate( $angle ) {
  272. if ( function_exists('imagerotate') ) {
  273. $transparency = imagecolorallocatealpha( $this->image, 255, 255, 255, 127 );
  274. $rotated = imagerotate( $this->image, $angle, $transparency );
  275. if ( is_resource( $rotated ) ) {
  276. imagealphablending( $rotated, true );
  277. imagesavealpha( $rotated, true );
  278. imagedestroy( $this->image );
  279. $this->image = $rotated;
  280. $this->update_size();
  281. return true;
  282. }
  283. }
  284. return new WP_Error( 'image_rotate_error', __('Image rotate failed.'), $this->file );
  285. }
  286. /**
  287. * Flips current image.
  288. *
  289. * @since 3.5.0
  290. * @access public
  291. *
  292. * @param bool $horz Flip along Horizontal Axis
  293. * @param bool $vert Flip along Vertical Axis
  294. * @return true|WP_Error
  295. */
  296. public function flip( $horz, $vert ) {
  297. $w = $this->size['width'];
  298. $h = $this->size['height'];
  299. $dst = wp_imagecreatetruecolor( $w, $h );
  300. if ( is_resource( $dst ) ) {
  301. $sx = $vert ? ($w - 1) : 0;
  302. $sy = $horz ? ($h - 1) : 0;
  303. $sw = $vert ? -$w : $w;
  304. $sh = $horz ? -$h : $h;
  305. if ( imagecopyresampled( $dst, $this->image, 0, 0, $sx, $sy, $w, $h, $sw, $sh ) ) {
  306. imagedestroy( $this->image );
  307. $this->image = $dst;
  308. return true;
  309. }
  310. }
  311. return new WP_Error( 'image_flip_error', __('Image flip failed.'), $this->file );
  312. }
  313. /**
  314. * Saves current in-memory image to file.
  315. *
  316. * @since 3.5.0
  317. * @access public
  318. *
  319. * @param string|null $filename
  320. * @param string|null $mime_type
  321. * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
  322. */
  323. public function save( $filename = null, $mime_type = null ) {
  324. $saved = $this->_save( $this->image, $filename, $mime_type );
  325. if ( ! is_wp_error( $saved ) ) {
  326. $this->file = $saved['path'];
  327. $this->mime_type = $saved['mime-type'];
  328. }
  329. return $saved;
  330. }
  331. /**
  332. * @param resource $image
  333. * @param string|null $filename
  334. * @param string|null $mime_type
  335. * @return WP_Error|array
  336. */
  337. protected function _save( $image, $filename = null, $mime_type = null ) {
  338. list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
  339. if ( ! $filename )
  340. $filename = $this->generate_filename( null, null, $extension );
  341. if ( 'image/gif' == $mime_type ) {
  342. if ( ! $this->make_image( $filename, 'imagegif', array( $image, $filename ) ) )
  343. return new WP_Error( 'image_save_error', __('Image Editor Save Failed') );
  344. }
  345. elseif ( 'image/png' == $mime_type ) {
  346. // convert from full colors to index colors, like original PNG.
  347. if ( function_exists('imageistruecolor') && ! imageistruecolor( $image ) )
  348. imagetruecolortopalette( $image, false, imagecolorstotal( $image ) );
  349. if ( ! $this->make_image( $filename, 'imagepng', array( $image, $filename ) ) )
  350. return new WP_Error( 'image_save_error', __('Image Editor Save Failed') );
  351. }
  352. elseif ( 'image/jpeg' == $mime_type ) {
  353. if ( ! $this->make_image( $filename, 'imagejpeg', array( $image, $filename, $this->get_quality() ) ) )
  354. return new WP_Error( 'image_save_error', __('Image Editor Save Failed') );
  355. }
  356. else {
  357. return new WP_Error( 'image_save_error', __('Image Editor Save Failed') );
  358. }
  359. // Set correct file permissions
  360. $stat = stat( dirname( $filename ) );
  361. $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
  362. @ chmod( $filename, $perms );
  363. /**
  364. * Filters the name of the saved image file.
  365. *
  366. * @since 2.6.0
  367. *
  368. * @param string $filename Name of the file.
  369. */
  370. return array(
  371. 'path' => $filename,
  372. 'file' => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
  373. 'width' => $this->size['width'],
  374. 'height' => $this->size['height'],
  375. 'mime-type' => $mime_type,
  376. );
  377. }
  378. /**
  379. * Returns stream of current image.
  380. *
  381. * @since 3.5.0
  382. * @access public
  383. *
  384. * @param string $mime_type
  385. * @return bool
  386. */
  387. public function stream( $mime_type = null ) {
  388. list( $filename, $extension, $mime_type ) = $this->get_output_format( null, $mime_type );
  389. switch ( $mime_type ) {
  390. case 'image/png':
  391. header( 'Content-Type: image/png' );
  392. return imagepng( $this->image );
  393. case 'image/gif':
  394. header( 'Content-Type: image/gif' );
  395. return imagegif( $this->image );
  396. default:
  397. header( 'Content-Type: image/jpeg' );
  398. return imagejpeg( $this->image, null, $this->get_quality() );
  399. }
  400. }
  401. /**
  402. * Either calls editor's save function or handles file as a stream.
  403. *
  404. * @since 3.5.0
  405. * @access protected
  406. *
  407. * @param string|stream $filename
  408. * @param callable $function
  409. * @param array $arguments
  410. * @return bool
  411. */
  412. protected function make_image( $filename, $function, $arguments ) {
  413. if ( wp_is_stream( $filename ) )
  414. $arguments[1] = null;
  415. return parent::make_image( $filename, $function, $arguments );
  416. }
  417. }