Aucune description

class-wp-image-editor.php 16KB

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