class-wp-image-editor.php 17 KB

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