balmet.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-image-editor.php (16586B)


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