WordPress/wp-includes/class-wp-image-editor.php

658 lines
17 KiB
PHP
Raw Normal View History

<?php
/**
* Base WordPress Image Editor
*
* @package WordPress
* @subpackage Image_Editor
*/
/**
* Base image editor class from which implementations extend
*
* @since 3.5.0
*/
Code Modernization: Add `AllowDynamicProperties` attribute to all (parent) classes. Dynamic (non-explicitly declared) properties are deprecated as of PHP 8.2 and are expected to become a fatal error in PHP 9.0. There are a number of ways to mitigate this: * If it is an accidental typo for a declared property: fix the typo. * For known properties: declare them on the class. * For unknown properties: add the magic `__get()`, `__set()`, et al. methods to the class or let the class extend `stdClass` which has highly optimized versions of these magic methods built in. * For unknown ''use'' of dynamic properties, the `#[AllowDynamicProperties]` attribute can be added to the class. The attribute will automatically be inherited by child classes. Trac ticket #56034 is open to investigate and handle the third and fourth type of situations, however it has become clear this will need more time and will not be ready in time for WP 6.1. To reduce “noise” in the meantime, both in the error logs of WP users moving onto PHP 8.2, in the test run logs of WP itself, in test runs of plugins and themes, as well as to prevent duplicate tickets from being opened for the same issue, this commit adds the `#[AllowDynamicProperties]` attribute to all “parent” classes in WP. The logic used for this commit is as follows: * If a class already has the attribute: no action needed. * If a class does not `extend`: add the attribute. * If a class does `extend`: - If it extends `stdClass`: no action needed (as `stdClass` supports dynamic properties). - If it extends a PHP native class: add the attribute. - If it extends a class from one of WP's external dependencies: add the attribute. * In all other cases: no action — the attribute should not be needed as child classes inherit from the parent. Whether or not a class contains magic methods has not been taken into account, as a review of the currently existing magic methods has shown that those are generally not sturdy enough and often even set dynamic properties (which they should not). See the [https://www.youtube.com/watch?v=vDZWepDQQVE live stream from August 16, 2022] for more details. This commit only affects classes in the `src` directory of WordPress core. * Tests should not get this attribute, but should be fixed to not use dynamic properties instead. Patches for this are already being committed under ticket #56033. * While a number bundled themes (2014, 2019, 2020, 2021) contain classes, they are not a part of this commit and may be updated separately. Reference: [https://wiki.php.net/rfc/deprecate_dynamic_properties PHP RFC: Deprecate dynamic properties]. Follow-up to [53922]. Props jrf, hellofromTonya, markjaquith, peterwilsoncc, costdev, knutsp, aristath. See #56513, #56034. Built from https://develop.svn.wordpress.org/trunk@54133 git-svn-id: http://core.svn.wordpress.org/trunk@53692 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2022-09-12 11:47:14 -04:00
#[AllowDynamicProperties]
abstract class WP_Image_Editor {
protected $file = null;
protected $size = null;
protected $mime_type = null;
protected $output_mime_type = null;
protected $default_mime_type = 'image/jpeg';
protected $quality = false;
// Deprecated since 5.8.1. See get_default_quality() below.
protected $default_quality = 82;
/**
* Each instance handles a single file.
*
* @param string $file Path to the file to load.
*/
public function __construct( $file ) {
$this->file = $file;
}
/**
* Checks to see if current environment supports the editor chosen.
* Must be overridden in a subclass.
*
* @since 3.5.0
*
* @abstract
*
* @param array $args
* @return bool
*/
public static function test( $args = array() ) {
return false;
}
/**
* Checks to see if editor supports the mime-type specified.
* Must be overridden in a subclass.
*
* @since 3.5.0
*
* @abstract
*
* @param string $mime_type
* @return bool
*/
public static function supports_mime_type( $mime_type ) {
return false;
}
/**
* Loads image from $this->file into editor.
*
* @since 3.5.0
* @abstract
*
* @return true|WP_Error True if loaded; WP_Error on failure.
*/
abstract public function load();
/**
* Saves current image to file.
*
* @since 3.5.0
* @since 6.0.0 The `$filesize` value was added to the returned array.
* @abstract
*
* @param string $destfilename Optional. Destination filename. Default null.
* @param string $mime_type Optional. The mime-type. Default null.
* @return array|WP_Error {
* Array on success or WP_Error if the file failed to save.
*
* @type string $path Path to the image file.
* @type string $file Name of the image file.
* @type int $width Image width.
* @type int $height Image height.
* @type string $mime-type The mime type of the image.
* @type int $filesize File size of the image.
* }
*/
abstract public function save( $destfilename = null, $mime_type = null );
/**
* Resizes current image.
*
* At minimum, either a height or width must be provided.
* If one of the two is set to null, the resize will
* maintain aspect ratio according to the provided dimension.
*
* @since 3.5.0
* @abstract
*
* @param int|null $max_w Image width.
* @param int|null $max_h Image height.
* @param bool|array $crop {
* Optional. Image cropping behavior. If false, the image will be scaled (default).
* If true, image will be cropped to the specified dimensions using center positions.
* If an array, the image will be cropped using the array to specify the crop location:
*
* @type string $0 The x crop position. Accepts 'left' 'center', or 'right'.
* @type string $1 The y crop position. Accepts 'top', 'center', or 'bottom'.
* }
* @return true|WP_Error
*/
abstract public function resize( $max_w, $max_h, $crop = false );
/**
* Resize multiple images from a single source.
*
* @since 3.5.0
* @abstract
*
* @param array $sizes {
* An array of image size arrays. Default sizes are 'small', 'medium', 'large'.
*
* @type array ...$0 {
* @type int $width Image width.
* @type int $height Image height.
* @type bool|array $crop Optional. Whether to crop the image. Default false.
* }
* }
* @return array An array of resized images metadata by size.
*/
abstract public function multi_resize( $sizes );
/**
* Crops Image.
*
* @since 3.5.0
* @abstract
*
* @param int $src_x The start x position to crop from.
* @param int $src_y The start y position to crop from.
* @param int $src_w The width to crop.
* @param int $src_h The height to crop.
* @param int $dst_w Optional. The destination width.
* @param int $dst_h Optional. The destination height.
* @param bool $src_abs Optional. If the source crop points are absolute.
* @return true|WP_Error
*/
abstract public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false );
/**
* Rotates current image counter-clockwise by $angle.
*
* @since 3.5.0
* @abstract
*
* @param float $angle
* @return true|WP_Error
*/
abstract public function rotate( $angle );
/**
* Flips current image.
*
* @since 3.5.0
* @abstract
*
* @param bool $horz Flip along Horizontal Axis
* @param bool $vert Flip along Vertical Axis
* @return true|WP_Error
*/
abstract public function flip( $horz, $vert );
/**
* Streams current image to browser.
*
* @since 3.5.0
* @abstract
*
* @param string $mime_type The mime type of the image.
* @return true|WP_Error True on success, WP_Error object on failure.
*/
abstract public function stream( $mime_type = null );
/**
* Gets dimensions of image.
*
* @since 3.5.0
*
* @return int[] {
* Dimensions of the image.
*
* @type int $width The image width.
* @type int $height The image height.
* }
*/
public function get_size() {
return $this->size;
}
/**
* Sets current image size.
*
* @since 3.5.0
*
* @param int $width
* @param int $height
* @return true
*/
protected function update_size( $width = null, $height = null ) {
$this->size = array(
'width' => (int) $width,
'height' => (int) $height,
);
return true;
}
/**
* Gets the Image Compression quality on a 1-100% scale.
*
* @since 4.0.0
*
* @return int Compression Quality. Range: [1,100]
*/
public function get_quality() {
if ( ! $this->quality ) {
$this->set_quality();
}
return $this->quality;
}
/**
* Sets Image Compression quality on a 1-100% scale.
*
* @since 3.5.0
*
* @param int $quality Compression Quality. Range: [1,100]
* @return true|WP_Error True if set successfully; WP_Error on failure.
*/
public function set_quality( $quality = null ) {
// Use the output mime type if present. If not, fall back to the input/initial mime type.
$mime_type = ! empty( $this->output_mime_type ) ? $this->output_mime_type : $this->mime_type;
// Get the default quality setting for the mime type.
$default_quality = $this->get_default_quality( $mime_type );
if ( null === $quality ) {
/**
* Filters the default image compression quality setting.
*
* Applies only during initial editor instantiation, or when set_quality() is run
* manually without the `$quality` argument.
*
* The WP_Image_Editor::set_quality() method has priority over the filter.
*
* @since 3.5.0
*
* @param int $quality Quality level between 1 (low) and 100 (high).
* @param string $mime_type Image mime type.
*/
$quality = apply_filters( 'wp_editor_set_quality', $default_quality, $mime_type );
if ( 'image/jpeg' === $mime_type ) {
/**
* Filters the JPEG compression quality for backward-compatibility.
*
* Applies only during initial editor instantiation, or when set_quality() is run
* manually without the `$quality` argument.
*
* The WP_Image_Editor::set_quality() method has priority over the filter.
*
* The filter is evaluated under two contexts: 'image_resize', and 'edit_image',
* (when a JPEG image is saved to file).
*
* @since 2.5.0
*
* @param int $quality Quality level between 0 (low) and 100 (high) of the JPEG.
* @param string $context Context of the filter.
*/
$quality = apply_filters( 'jpeg_quality', $quality, 'image_resize' );
}
if ( $quality < 0 || $quality > 100 ) {
$quality = $default_quality;
}
}
// Allow 0, but squash to 1 due to identical images in GD, and for backward compatibility.
if ( 0 === $quality ) {
$quality = 1;
}
if ( ( $quality >= 1 ) && ( $quality <= 100 ) ) {
$this->quality = $quality;
return true;
} else {
return new WP_Error( 'invalid_image_quality', __( 'Attempted to set image quality outside of the range [1,100].' ) );
}
}
/**
* Returns the default compression quality setting for the mime type.
*
* @since 5.8.1
*
* @param string $mime_type
* @return int The default quality setting for the mime type.
*/
protected function get_default_quality( $mime_type ) {
switch ( $mime_type ) {
case 'image/webp':
$quality = 86;
break;
case 'image/jpeg':
case 'image/avif':
default:
$quality = $this->default_quality;
}
return $quality;
}
/**
* Returns preferred mime-type and extension based on provided
* file's extension and mime, or current file's extension and mime.
*
* Will default to $this->default_mime_type if requested is not supported.
*
* Provides corrected filename only if filename is provided.
*
* @since 3.5.0
*
* @param string $filename
* @param string $mime_type
* @return array { filename|null, extension, mime-type }
*/
protected function get_output_format( $filename = null, $mime_type = null ) {
$new_ext = null;
// By default, assume specified type takes priority.
if ( $mime_type ) {
$new_ext = $this->get_extension( $mime_type );
}
if ( $filename ) {
$file_ext = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );
$file_mime = $this->get_mime_type( $file_ext );
} else {
// If no file specified, grab editor's current extension and mime-type.
$file_ext = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
$file_mime = $this->mime_type;
}
/*
* Check to see if specified mime-type is the same as type implied by
* file extension. If so, prefer extension from file.
*/
if ( ! $mime_type || ( $file_mime === $mime_type ) ) {
$mime_type = $file_mime;
$new_ext = $file_ext;
}
/**
* Filters the image editor output format mapping.
*
* Enables filtering the mime type used to save images. By default,
* the mapping array is empty, so the mime type matches the source image.
*
* @see WP_Image_Editor::get_output_format()
*
* @since 5.8.0
*
* @param string[] $output_format {
* An array of mime type mappings. Maps a source mime type to a new
* destination mime type. Default empty array.
*
* @type string ...$0 The new mime type.
* }
* @param string $filename Path to the image.
* @param string $mime_type The source image mime type.
*/
$output_format = apply_filters( 'image_editor_output_format', array(), $filename, $mime_type );
if ( isset( $output_format[ $mime_type ] )
&& $this->supports_mime_type( $output_format[ $mime_type ] )
) {
$mime_type = $output_format[ $mime_type ];
$new_ext = $this->get_extension( $mime_type );
}
/*
* Double-check that the mime-type selected is supported by the editor.
* If not, choose a default instead.
*/
if ( ! $this->supports_mime_type( $mime_type ) ) {
/**
* Filters default mime type prior to getting the file extension.
*
* @see wp_get_mime_types()
*
* @since 3.5.0
*
* @param string $mime_type Mime type string.
*/
$mime_type = apply_filters( 'image_editor_default_mime_type', $this->default_mime_type );
$new_ext = $this->get_extension( $mime_type );
}
/*
* Ensure both $filename and $new_ext are not empty.
* $this->get_extension() returns false on error which would effectively remove the extension
* from $filename. That shouldn't happen, files without extensions are not supported.
*/
if ( $filename && $new_ext ) {
$dir = pathinfo( $filename, PATHINFO_DIRNAME );
$ext = pathinfo( $filename, PATHINFO_EXTENSION );
$filename = trailingslashit( $dir ) . wp_basename( $filename, ".$ext" ) . ".{$new_ext}";
}
if ( $mime_type && ( $mime_type !== $this->mime_type ) ) {
// The image will be converted when saving. Set the quality for the new mime-type if not already set.
if ( $mime_type !== $this->output_mime_type ) {
$this->output_mime_type = $mime_type;
}
$this->set_quality();
} elseif ( ! empty( $this->output_mime_type ) ) {
// Reset output_mime_type and quality.
$this->output_mime_type = null;
$this->set_quality();
}
return array( $filename, $new_ext, $mime_type );
}
/**
* Builds an output filename based on current file, and adding proper suffix
*
* @since 3.5.0
*
* @param string $suffix
* @param string $dest_path
* @param string $extension
* @return string filename
*/
public function generate_filename( $suffix = null, $dest_path = null, $extension = null ) {
// $suffix will be appended to the destination filename, just before the extension.
if ( ! $suffix ) {
$suffix = $this->get_suffix();
}
$dir = pathinfo( $this->file, PATHINFO_DIRNAME );
$ext = pathinfo( $this->file, PATHINFO_EXTENSION );
$name = wp_basename( $this->file, ".$ext" );
$new_ext = strtolower( $extension ? $extension : $ext );
if ( ! is_null( $dest_path ) ) {
if ( ! wp_is_stream( $dest_path ) ) {
$_dest_path = realpath( $dest_path );
if ( $_dest_path ) {
$dir = $_dest_path;
}
} else {
$dir = $dest_path;
}
}
return trailingslashit( $dir ) . "{$name}-{$suffix}.{$new_ext}";
}
/**
* Builds and returns proper suffix for file based on height and width.
*
* @since 3.5.0
*
* @return string|false suffix
*/
public function get_suffix() {
if ( ! $this->get_size() ) {
return false;
}
return "{$this->size['width']}x{$this->size['height']}";
}
/**
* Check if a JPEG image has EXIF Orientation tag and rotate it if needed.
*
* @since 5.3.0
*
* @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).
* WP_Error if error while rotating.
*/
public function maybe_exif_rotate() {
$orientation = null;
if ( is_callable( 'exif_read_data' ) && 'image/jpeg' === $this->mime_type ) {
$exif_data = @exif_read_data( $this->file );
if ( ! empty( $exif_data['Orientation'] ) ) {
$orientation = (int) $exif_data['Orientation'];
}
}
/**
* Filters the `$orientation` value to correct it before rotating or to prevent rotating the image.
*
* @since 5.3.0
*
* @param int $orientation EXIF Orientation value as retrieved from the image file.
* @param string $file Path to the image file.
*/
$orientation = apply_filters( 'wp_image_maybe_exif_rotate', $orientation, $this->file );
if ( ! $orientation || 1 === $orientation ) {
return false;
}
switch ( $orientation ) {
case 2:
// Flip horizontally.
$result = $this->flip( false, true );
break;
case 3:
/*
* Rotate 180 degrees or flip horizontally and vertically.
* Flipping seems faster and uses less resources.
*/
$result = $this->flip( true, true );
break;
case 4:
// Flip vertically.
$result = $this->flip( true, false );
break;
case 5:
// Rotate 90 degrees counter-clockwise and flip vertically.
$result = $this->rotate( 90 );
if ( ! is_wp_error( $result ) ) {
$result = $this->flip( true, false );
}
break;
case 6:
// Rotate 90 degrees clockwise (270 counter-clockwise).
$result = $this->rotate( 270 );
break;
case 7:
// Rotate 90 degrees counter-clockwise and flip horizontally.
$result = $this->rotate( 90 );
if ( ! is_wp_error( $result ) ) {
$result = $this->flip( false, true );
}
break;
case 8:
// Rotate 90 degrees counter-clockwise.
$result = $this->rotate( 90 );
break;
}
return $result;
}
/**
* Either calls editor's save function or handles file as a stream.
*
* @since 3.5.0
*
* @param string $filename
* @param callable $callback
* @param array $arguments
* @return bool
*/
protected function make_image( $filename, $callback, $arguments ) {
$stream = wp_is_stream( $filename );
if ( $stream ) {
ob_start();
} else {
// The directory containing the original file may no longer exist when using a replication plugin.
wp_mkdir_p( dirname( $filename ) );
}
$result = call_user_func_array( $callback, $arguments );
if ( $result && $stream ) {
$contents = ob_get_contents();
$fp = fopen( $filename, 'w' );
if ( ! $fp ) {
ob_end_clean();
return false;
}
fwrite( $fp, $contents );
fclose( $fp );
}
if ( $stream ) {
ob_end_clean();
}
return $result;
}
/**
* Returns first matched mime-type from extension,
* as mapped from wp_get_mime_types()
*
* @since 3.5.0
*
* @param string $extension
* @return string|false
*/
protected static function get_mime_type( $extension = null ) {
if ( ! $extension ) {
return false;
}
$mime_types = wp_get_mime_types();
$extensions = array_keys( $mime_types );
foreach ( $extensions as $_extension ) {
if ( preg_match( "/{$extension}/i", $_extension ) ) {
return $mime_types[ $_extension ];
}
}
return false;
}
/**
* Returns first matched extension from Mime-type,
* as mapped from wp_get_mime_types()
*
* @since 3.5.0
*
* @param string $mime_type
* @return string|false
*/
protected static function get_extension( $mime_type = null ) {
if ( empty( $mime_type ) ) {
return false;
}
return wp_get_default_extension_for_mime_type( $mime_type );
}
Media: enable generating multiple mime types for image uploads; specifically WebP versions for JPEG images by default. This changeset adds the capability for core media uploads to generate sub sized images in more than a single mime type. The output formats for each mime type can be controlled through a filter. WebP is used as an additional output format for JPEG images by default to improve front end performance. When generating additional mime types, only images which are smaller than the respective original are retained. By default, additional mime type images are only generated for the built-in core image sizes and any custom sizes that have opted in. Image meta is updated with a new 'sources' array containing file details for each mime type. Each image size in the 'sizes' array also gets a new 'sources' array that contains the image file details for each mime type. This change also increases image upload retries to accommodate additional image sizes. It also adds a `$mime_type` parameter to the `wp_get_missing_image_subsizes` function and filter. This change adds three new filters to enable full control of secondary mime image generation and output: * A new filter `wp_image_sizes_with_additional_mime_type_support` that filters the sizes that support secondary mime type output. Developers can use this to control the output of additional mime type sub-sized images on a per size basis. * A new filter `wp_upload_image_mime_transforms` that filters the output mime types for a given input mime type. Developers can use this to control generation of additional mime types for a given input mime type or even override the original mime type. * A new filter `wp_content_image_mimes` which controls image mime type output selection and order for frontend content. Developers can use this to control the mime type output preference order for content images. Content images inserted from the media library will use the available image versions based on the order from this filter. Thanks to the many contributors who helped develop, test and give feedback on this feature. A haiku to summarize: Upload a JPEG Images of all sizes Output as WebPs Props flixos90, MatthiasReinholz, studiolxv, markhowellsmead, eatingrules, pbiron, mukesh27, joegrainger, mehulkaklotar, tweetythierry, akshitsethi, peterwilsoncc, eugenemanuilov, mitogh, shetheliving, clarkeemily, codekraft, mikeschroder, clorith, kasparsd, spacedmonkey, trevorpfromsandee, jb510, scofennellgmailcom, seedsca, cagsmith, karinclimber, dainemawer, baxbridge, grapplerulrich, sobatkras, chynnabenton, tonylocalword, barneydavey, kwillmorth, garymatthews919, olliejones, imarkinteractive, jeffpaul, feastdesignco, webbeetle, masteradhoc. See #55443. Built from https://develop.svn.wordpress.org/trunk@53751 git-svn-id: http://core.svn.wordpress.org/trunk@53310 1a063a9b-81f0-0310-95a4-ce76da25c4cd
2022-07-21 14:03:13 -04:00
}