class-wp-image-editor.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. <?php
  2. /**
  3. * Base WordPress Image Editor
  4. *
  5. * @package WordPress
  6. * @subpackage Image_Editor
  7. */
  8. /**
  9. * Base image editor class from which implementations extend
  10. *
  11. * @since 3.5.0
  12. */
  13. abstract class WP_Image_Editor {
  14. protected $file = null;
  15. protected $size = null;
  16. protected $mime_type = null;
  17. protected $default_mime_type = 'image/jpeg';
  18. protected $quality = false;
  19. protected $default_quality = 82;
  20. /**
  21. * Each instance handles a single file.
  22. *
  23. * @param string $file Path to the file to load.
  24. */
  25. public function __construct( $file ) {
  26. $this->file = $file;
  27. }
  28. /**
  29. * Checks to see if current environment supports the editor chosen.
  30. * Must be overridden in a sub-class.
  31. *
  32. * @since 3.5.0
  33. *
  34. * @abstract
  35. *
  36. * @param array $args
  37. * @return bool
  38. */
  39. public static function test( $args = array() ) {
  40. return false;
  41. }
  42. /**
  43. * Checks to see if editor supports the mime-type specified.
  44. * Must be overridden in a sub-class.
  45. *
  46. * @since 3.5.0
  47. *
  48. * @abstract
  49. *
  50. * @param string $mime_type
  51. * @return bool
  52. */
  53. public static function supports_mime_type( $mime_type ) {
  54. return false;
  55. }
  56. /**
  57. * Loads image from $this->file into editor.
  58. *
  59. * @since 3.5.0
  60. * @abstract
  61. *
  62. * @return bool|WP_Error True if loaded; WP_Error on failure.
  63. */
  64. abstract public function load();
  65. /**
  66. * Saves current image to file.
  67. *
  68. * @since 3.5.0
  69. * @abstract
  70. *
  71. * @param string $destfilename
  72. * @param string $mime_type
  73. * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
  74. */
  75. abstract public function save( $destfilename = null, $mime_type = null );
  76. /**
  77. * Resizes current image.
  78. *
  79. * At minimum, either a height or width must be provided.
  80. * If one of the two is set to null, the resize will
  81. * maintain aspect ratio according to the provided dimension.
  82. *
  83. * @since 3.5.0
  84. * @abstract
  85. *
  86. * @param int|null $max_w Image width.
  87. * @param int|null $max_h Image height.
  88. * @param bool $crop
  89. * @return bool|WP_Error
  90. */
  91. abstract public function resize( $max_w, $max_h, $crop = false );
  92. /**
  93. * Resize multiple images from a single source.
  94. *
  95. * @since 3.5.0
  96. * @abstract
  97. *
  98. * @param array $sizes {
  99. * An array of image size arrays. Default sizes are 'small', 'medium', 'large'.
  100. *
  101. * @type array $size {
  102. * @type int $width Image width.
  103. * @type int $height Image height.
  104. * @type bool $crop Optional. Whether to crop the image. Default false.
  105. * }
  106. * }
  107. * @return array An array of resized images metadata by size.
  108. */
  109. abstract public function multi_resize( $sizes );
  110. /**
  111. * Crops Image.
  112. *
  113. * @since 3.5.0
  114. * @abstract
  115. *
  116. * @param int $src_x The start x position to crop from.
  117. * @param int $src_y The start y position to crop from.
  118. * @param int $src_w The width to crop.
  119. * @param int $src_h The height to crop.
  120. * @param int $dst_w Optional. The destination width.
  121. * @param int $dst_h Optional. The destination height.
  122. * @param bool $src_abs Optional. If the source crop points are absolute.
  123. * @return bool|WP_Error
  124. */
  125. abstract public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false );
  126. /**
  127. * Rotates current image counter-clockwise by $angle.
  128. *
  129. * @since 3.5.0
  130. * @abstract
  131. *
  132. * @param float $angle
  133. * @return bool|WP_Error
  134. */
  135. abstract public function rotate( $angle );
  136. /**
  137. * Flips current image.
  138. *
  139. * @since 3.5.0
  140. * @abstract
  141. *
  142. * @param bool $horz Flip along Horizontal Axis
  143. * @param bool $vert Flip along Vertical Axis
  144. * @return bool|WP_Error
  145. */
  146. abstract public function flip( $horz, $vert );
  147. /**
  148. * Streams current image to browser.
  149. *
  150. * @since 3.5.0
  151. * @abstract
  152. *
  153. * @param string $mime_type The mime type of the image.
  154. * @return bool|WP_Error True on success, WP_Error object or false on failure.
  155. */
  156. abstract public function stream( $mime_type = null );
  157. /**
  158. * Gets dimensions of image.
  159. *
  160. * @since 3.5.0
  161. *
  162. * @return array {'width'=>int, 'height'=>int}
  163. */
  164. public function get_size() {
  165. return $this->size;
  166. }
  167. /**
  168. * Sets current image size.
  169. *
  170. * @since 3.5.0
  171. *
  172. * @param int $width
  173. * @param int $height
  174. * @return true
  175. */
  176. protected function update_size( $width = null, $height = null ) {
  177. $this->size = array(
  178. 'width' => (int) $width,
  179. 'height' => (int) $height,
  180. );
  181. return true;
  182. }
  183. /**
  184. * Gets the Image Compression quality on a 1-100% scale.
  185. *
  186. * @since 4.0.0
  187. *
  188. * @return int $quality Compression Quality. Range: [1,100]
  189. */
  190. public function get_quality() {
  191. if ( ! $this->quality ) {
  192. $this->set_quality();
  193. }
  194. return $this->quality;
  195. }
  196. /**
  197. * Sets Image Compression quality on a 1-100% scale.
  198. *
  199. * @since 3.5.0
  200. *
  201. * @param int $quality Compression Quality. Range: [1,100]
  202. * @return true|WP_Error True if set successfully; WP_Error on failure.
  203. */
  204. public function set_quality( $quality = null ) {
  205. if ( null === $quality ) {
  206. /**
  207. * Filters the default image compression quality setting.
  208. *
  209. * Applies only during initial editor instantiation, or when set_quality() is run
  210. * manually without the `$quality` argument.
  211. *
  212. * set_quality() has priority over the filter.
  213. *
  214. * @since 3.5.0
  215. *
  216. * @param int $quality Quality level between 1 (low) and 100 (high).
  217. * @param string $mime_type Image mime type.
  218. */
  219. $quality = apply_filters( 'wp_editor_set_quality', $this->default_quality, $this->mime_type );
  220. if ( 'image/jpeg' == $this->mime_type ) {
  221. /**
  222. * Filters the JPEG compression quality for backward-compatibility.
  223. *
  224. * Applies only during initial editor instantiation, or when set_quality() is run
  225. * manually without the `$quality` argument.
  226. *
  227. * set_quality() has priority over the filter.
  228. *
  229. * The filter is evaluated under two contexts: 'image_resize', and 'edit_image',
  230. * (when a JPEG image is saved to file).
  231. *
  232. * @since 2.5.0
  233. *
  234. * @param int $quality Quality level between 0 (low) and 100 (high) of the JPEG.
  235. * @param string $context Context of the filter.
  236. */
  237. $quality = apply_filters( 'jpeg_quality', $quality, 'image_resize' );
  238. }
  239. if ( $quality < 0 || $quality > 100 ) {
  240. $quality = $this->default_quality;
  241. }
  242. }
  243. // Allow 0, but squash to 1 due to identical images in GD, and for backward compatibility.
  244. if ( 0 === $quality ) {
  245. $quality = 1;
  246. }
  247. if ( ( $quality >= 1 ) && ( $quality <= 100 ) ) {
  248. $this->quality = $quality;
  249. return true;
  250. } else {
  251. return new WP_Error( 'invalid_image_quality', __( 'Attempted to set image quality outside of the range [1,100].' ) );
  252. }
  253. }
  254. /**
  255. * Returns preferred mime-type and extension based on provided
  256. * file's extension and mime, or current file's extension and mime.
  257. *
  258. * Will default to $this->default_mime_type if requested is not supported.
  259. *
  260. * Provides corrected filename only if filename is provided.
  261. *
  262. * @since 3.5.0
  263. *
  264. * @param string $filename
  265. * @param string $mime_type
  266. * @return array { filename|null, extension, mime-type }
  267. */
  268. protected function get_output_format( $filename = null, $mime_type = null ) {
  269. $new_ext = null;
  270. // By default, assume specified type takes priority
  271. if ( $mime_type ) {
  272. $new_ext = $this->get_extension( $mime_type );
  273. }
  274. if ( $filename ) {
  275. $file_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
  276. $file_mime = $this->get_mime_type( $file_ext );
  277. } else {
  278. // If no file specified, grab editor's current extension and mime-type.
  279. $file_ext = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
  280. $file_mime = $this->mime_type;
  281. }
  282. // Check to see if specified mime-type is the same as type implied by
  283. // file extension. If so, prefer extension from file.
  284. if ( ! $mime_type || ( $file_mime == $mime_type ) ) {
  285. $mime_type = $file_mime;
  286. $new_ext = $file_ext;
  287. }
  288. // Double-check that the mime-type selected is supported by the editor.
  289. // If not, choose a default instead.
  290. if ( ! $this->supports_mime_type( $mime_type ) ) {
  291. /**
  292. * Filters default mime type prior to getting the file extension.
  293. *
  294. * @see wp_get_mime_types()
  295. *
  296. * @since 3.5.0
  297. *
  298. * @param string $mime_type Mime type string.
  299. */
  300. $mime_type = apply_filters( 'image_editor_default_mime_type', $this->default_mime_type );
  301. $new_ext = $this->get_extension( $mime_type );
  302. }
  303. if ( $filename ) {
  304. $dir = pathinfo( $filename, PATHINFO_DIRNAME );
  305. $ext = pathinfo( $filename, PATHINFO_EXTENSION );
  306. $filename = trailingslashit( $dir ) . wp_basename( $filename, ".$ext" ) . ".{$new_ext}";
  307. }
  308. return array( $filename, $new_ext, $mime_type );
  309. }
  310. /**
  311. * Builds an output filename based on current file, and adding proper suffix
  312. *
  313. * @since 3.5.0
  314. *
  315. * @param string $suffix
  316. * @param string $dest_path
  317. * @param string $extension
  318. * @return string filename
  319. */
  320. public function generate_filename( $suffix = null, $dest_path = null, $extension = null ) {
  321. // $suffix will be appended to the destination filename, just before the extension
  322. if ( ! $suffix ) {
  323. $suffix = $this->get_suffix();
  324. }
  325. $dir = pathinfo( $this->file, PATHINFO_DIRNAME );
  326. $ext = pathinfo( $this->file, PATHINFO_EXTENSION );
  327. $name = wp_basename( $this->file, ".$ext" );
  328. $new_ext = strtolower( $extension ? $extension : $ext );
  329. if ( ! is_null( $dest_path ) ) {
  330. $_dest_path = realpath( $dest_path );
  331. if ( $_dest_path ) {
  332. $dir = $_dest_path;
  333. }
  334. }
  335. return trailingslashit( $dir ) . "{$name}-{$suffix}.{$new_ext}";
  336. }
  337. /**
  338. * Builds and returns proper suffix for file based on height and width.
  339. *
  340. * @since 3.5.0
  341. *
  342. * @return false|string suffix
  343. */
  344. public function get_suffix() {
  345. if ( ! $this->get_size() ) {
  346. return false;
  347. }
  348. return "{$this->size['width']}x{$this->size['height']}";
  349. }
  350. /**
  351. * Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
  352. *
  353. * @since 5.3.0
  354. *
  355. * @return bool|WP_Error True if the image was rotated. False if not rotated (no EXIF data or the image doesn't need to be rotated).
  356. * WP_Error if error while rotating.
  357. */
  358. public function maybe_exif_rotate() {
  359. $orientation = null;
  360. if ( is_callable( 'exif_read_data' ) && 'image/jpeg' === $this->mime_type ) {
  361. $exif_data = @exif_read_data( $this->file );
  362. if ( ! empty( $exif_data['Orientation'] ) ) {
  363. $orientation = (int) $exif_data['Orientation'];
  364. }
  365. }
  366. /**
  367. * Filters the `$orientation` value to correct it before rotating or to prevemnt rotating the image.
  368. *
  369. * @since 5.3.0
  370. *
  371. * @param int $orientation EXIF Orientation value as retrieved from the image file.
  372. * @param string $file Path to the image file.
  373. */
  374. $orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $this->file );
  375. if ( ! $orientation || $orientation === 1 ) {
  376. return false;
  377. }
  378. switch ( $orientation ) {
  379. case 2:
  380. // Flip horizontally.
  381. $result = $this->flip( true, false );
  382. break;
  383. case 3:
  384. // Rotate 180 degrees or flip horizontally and vertically.
  385. // Flipping seems faster/uses less resources.
  386. $result = $this->flip( true, true );
  387. break;
  388. case 4:
  389. // Flip vertically.
  390. $result = $this->flip( false, true );
  391. break;
  392. case 5:
  393. // Rotate 90 degrees counter-clockwise and flip vertically.
  394. $result = $this->rotate( 90 );
  395. if ( ! is_wp_error( $result ) ) {
  396. $result = $this->flip( false, true );
  397. }
  398. break;
  399. case 6:
  400. // Rotate 90 degrees clockwise (270 counter-clockwise).
  401. $result = $this->rotate( 270 );
  402. break;
  403. case 7:
  404. // Rotate 90 degrees counter-clockwise and flip horizontally.
  405. $result = $this->rotate( 90 );
  406. if ( ! is_wp_error( $result ) ) {
  407. $result = $this->flip( true, false );
  408. }
  409. break;
  410. case 8:
  411. // Rotate 90 degrees counter-clockwise.
  412. $result = $this->rotate( 90 );
  413. break;
  414. }
  415. return $result;
  416. }
  417. /**
  418. * Either calls editor's save function or handles file as a stream.
  419. *
  420. * @since 3.5.0
  421. *
  422. * @param string|stream $filename
  423. * @param callable $function
  424. * @param array $arguments
  425. * @return bool
  426. */
  427. protected function make_image( $filename, $function, $arguments ) {
  428. $stream = wp_is_stream( $filename );
  429. if ( $stream ) {
  430. ob_start();
  431. } else {
  432. // The directory containing the original file may no longer exist when using a replication plugin.
  433. wp_mkdir_p( dirname( $filename ) );
  434. }
  435. $result = call_user_func_array( $function, $arguments );
  436. if ( $result && $stream ) {
  437. $contents = ob_get_contents();
  438. $fp = fopen( $filename, 'w' );
  439. if ( ! $fp ) {
  440. ob_end_clean();
  441. return false;
  442. }
  443. fwrite( $fp, $contents );
  444. fclose( $fp );
  445. }
  446. if ( $stream ) {
  447. ob_end_clean();
  448. }
  449. return $result;
  450. }
  451. /**
  452. * Returns first matched mime-type from extension,
  453. * as mapped from wp_get_mime_types()
  454. *
  455. * @since 3.5.0
  456. *
  457. * @param string $extension
  458. * @return string|false
  459. */
  460. protected static function get_mime_type( $extension = null ) {
  461. if ( ! $extension ) {
  462. return false;
  463. }
  464. $mime_types = wp_get_mime_types();
  465. $extensions = array_keys( $mime_types );
  466. foreach ( $extensions as $_extension ) {
  467. if ( preg_match( "/{$extension}/i", $_extension ) ) {
  468. return $mime_types[ $_extension ];
  469. }
  470. }
  471. return false;
  472. }
  473. /**
  474. * Returns first matched extension from Mime-type,
  475. * as mapped from wp_get_mime_types()
  476. *
  477. * @since 3.5.0
  478. *
  479. * @param string $mime_type
  480. * @return string|false
  481. */
  482. protected static function get_extension( $mime_type = null ) {
  483. $extensions = explode( '|', array_search( $mime_type, wp_get_mime_types() ) );
  484. if ( empty( $extensions[0] ) ) {
  485. return false;
  486. }
  487. return $extensions[0];
  488. }
  489. }