WordPress 图片编辑器的幕后英雄:wp_get_image_editor()
深度解析
各位朋友,大家好!我是老码农,今天咱们来聊聊 WordPress 里一个经常被忽略,但又非常重要的函数:wp_get_image_editor()
。 它就像一位幕后英雄,默默地帮我们处理图片编辑的各种脏活累活,让我们在 WordPress 后台可以轻松地裁剪、缩放图片。
你可能觉得奇怪,图片处理不就是裁剪、缩放嘛,有啥复杂的?嘿,这里面的水可深着呢。不同的服务器环境,支持的图片处理库可能不一样,有的用 GD 库,有的用 Imagick。更麻烦的是,即使都支持,它们的用法、参数、效果也可能千差万别。
如果让我们自己去判断用哪个库,然后针对不同的库写不同的代码,那简直是噩梦!幸好,WordPress 的开发者们早就想到了这一点,他们用 wp_get_image_editor()
这个函数,把这些差异都封装起来了,给我们提供了一个统一的接口。
今天,咱们就来扒一扒 wp_get_image_editor()
的源码,看看它是如何根据服务器环境选择合适的图片编辑器,并封装成统一的接口的。准备好了吗?咱们开始!
1. 初探 wp_get_image_editor()
:你以为它很简单?
首先,我们来看看 wp-includes/media.php
文件里 wp_get_image_editor()
函数的庐山真面目:
function wp_get_image_editor( $file, $args = array() ) {
/**
* Filters the list of available image editors.
*
* @since 3.5.0
*
* @param string[] $editors An array of available image editors.
*/
$implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) );
if ( empty( $implementations ) ) {
return new WP_Error( 'image_no_editor', __( 'No image editor available.' ) );
}
foreach ( $implementations as $implementation ) {
if ( ! is_subclass_of( $implementation, 'WP_Image_Editor' ) ) {
continue;
}
if ( call_user_func( array( $implementation, 'test' ) ) ) {
$editor = new $implementation( $file, $args );
if ( is_wp_error( $editor->get_error_code() ) && $editor->get_error_code() !== 'error_loading_image' ) {
continue;
}
return $editor;
}
}
return new WP_Error( 'image_no_editor', __( 'No image editor available.' ) );
}
是不是觉得有点眼花缭乱?没关系,我们一步一步来分析。
$file
参数: 不用说,这是你要编辑的图片文件的路径。$args
参数: 可选参数,可以传递一些额外的配置信息,比如质量、大小等等。$implementations
数组: 这个数组里存放的是 WordPress 支持的图片编辑器类名。默认情况下,包含了WP_Image_Editor_Imagick
和WP_Image_Editor_GD
,分别代表 Imagick 和 GD 库。注意,这里用到了apply_filters
钩子,允许我们通过插件或主题来修改这个列表,添加或删除图片编辑器。- 循环遍历: 函数会遍历
$implementations
数组,依次尝试使用每个图片编辑器。 is_subclass_of()
: 首先,它会检查当前的类是否是WP_Image_Editor
的子类。WP_Image_Editor
是一个抽象类,定义了图片编辑器必须实现的一些基本方法。test()
方法: 关键的一步来了!call_user_func( array( $implementation, 'test' ) )
会调用当前图片编辑器类的test()
静态方法,来判断服务器环境是否支持该编辑器。- 实例化编辑器: 如果
test()
方法返回true
,表示支持,那么就实例化该编辑器,并返回。 - 错误处理: 如果在实例化过程中出现错误(比如图片加载失败),则继续尝试下一个编辑器。
- 找不到编辑器: 如果所有编辑器都尝试失败,则返回一个
WP_Error
对象,提示找不到可用的图片编辑器。
看到这里,你可能已经明白了 wp_get_image_editor()
的大致工作流程:它会根据 $implementations
数组,依次尝试使用每个图片编辑器,直到找到一个可用的为止。
2. WP_Image_Editor
抽象类:定义统一接口
刚才我们提到了 WP_Image_Editor
抽象类,它是所有图片编辑器类的基类,定义了统一的接口。 让我们来看看它的源码(位于 wp-includes/class-wp-image-editor.php
):
abstract class WP_Image_Editor {
/**
* File being manipulated.
*
* @since 3.5.0
* @var string
*/
protected $file;
/**
* Original image width.
*
* @since 3.5.0
* @var int
*/
protected $width;
/**
* Original image height.
*
* @since 3.5.0
* @var int
*/
protected $height;
/**
* Image quality.
*
* @since 3.5.0
* @var int
*/
protected $quality;
/**
* Stores whether errors have occurred.
*
* @since 3.5.0
* @var WP_Error
*/
protected $errors;
/**
* PHP4 constructor.
*
* @deprecated 3.5.0 Use __construct() instead.
*/
public function WP_Image_Editor( $file ) {
_deprecated_constructor( __CLASS__, '3.5.0', 'WP_Image_Editor::__construct()' );
self::__construct( $file );
}
/**
* WP_Image_Editor constructor.
*
* @since 3.5.0
*
* @param string $file Filename of the image to load.
*/
public function __construct( $file ) {
$this->file = $file;
$this->errors = new WP_Error();
}
/**
* Checks to see if there are any errors.
*
* @since 3.5.0
*
* @return string|false Error message on failure. False on success.
*/
public function get_error_message() {
if ( ! is_wp_error( $this->errors ) ) {
return false;
}
return $this->errors->get_error_message();
}
/**
* Returns a WP_Error object containing any errors.
*
* @since 3.5.0
*
* @return WP_Error WP_Error object.
*/
public function get_error() {
return $this->errors;
}
/**
* Returns an error code if one exists.
*
* @since 4.6.0
*
* @return string|false Error code on failure. False on success.
*/
public function get_error_code() {
if ( ! is_wp_error( $this->errors ) ) {
return false;
}
return $this->errors->get_error_code();
}
/**
* Checks to see if there are any errors.
*
* @since 3.5.0
*
* @return bool True if there are errors.
*/
public function has_errors() {
return (bool) count( $this->errors->get_error_codes() );
}
/**
* Adds an error.
*
* @since 3.5.0
*
* @param string|WP_Error $code Error code.
* @param string $message Error message.
*/
public function add_error( $code, $message = '' ) {
if ( is_wp_error( $code ) ) {
$this->errors->add( $code->get_error_code(), $code->get_error_message() );
} else {
$this->errors->add( $code, $message );
}
}
/**
* Removes all errors.
*
* @since 3.5.0
*/
public function remove_error() {
$this->errors = new WP_Error();
}
/**
* Sets the file property.
*
* @since 3.5.0
*
* @param string $file Path to the original image file.
*/
public function set_file( $file ) {
$this->file = $file;
}
/**
* Loads an image resource for editing.
*
* @since 3.5.0
*
* @return true|WP_Error True if loaded successfully. WP_Error on failure.
*/
abstract public function load();
/**
* Saves current image to file.
*
* @since 3.5.0
*
* @param string|null $destfilename Optional. String containing the file name to save to. Defaults to the original file.
* @param string|null $mime_type Optional. Image mime type. Defaults to the mime type of the file.
*
* @return array|WP_Error Array containing new width, height, and file name on success. WP_Error object on failure.
*/
abstract public function save( $destfilename = null, $mime_type = null );
/**
* Resizes current image.
*
* @since 3.5.0
*
* @param int|null $max_w Optional. Maximum width.
* @param int|null $max_h Optional. Maximum height.
* @param bool $crop Optional. Whether to crop image to specified dimensions.
*
* @return true|WP_Error True if resize is successful. WP_Error on failure.
*/
abstract public function resize( $max_w = null, $max_h = null, $crop = false );
/**
* Returns current image dimensions.
*
* @since 3.5.0
*
* @return array|WP_Error Associative array width and height on success. WP_Error on failure.
*/
abstract public function get_size();
/**
* Resizes and crops current image.
*
* @since 3.5.0
*
* @param int $dst_x The start x coordinate to crop from.
* @param int $dst_y The start y coordinate to crop from.
* @param int $src_w The width to crop.
* @param int $src_h The height to crop.
* @param int $dst_w The destination width.
* @param int $dst_h The destination height.
* @param bool $crop Whether to crop the image.
*
* @return true|WP_Error True on success. WP_Error on failure.
*/
abstract public function crop( $dst_x, $dst_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $crop = false );
/**
* Rotates current image.
*
* @since 3.5.0
*
* @param float $angle Angle in degrees to rotate.
*
* @return true|WP_Error True on success. WP_Error on failure.
*/
abstract public function rotate( $angle );
/**
* Flips current image.
*
* @since 3.5.0
*
* @param bool $horz Flip along the horizontal axis.
* @param bool $vert Flip along the vertical axis.
*
* @return true|WP_Error True on success. WP_Error on failure.
*/
abstract public function flip( $horz, $vert );
/**
* Saves current image to file.
*
* @since 3.5.0
*
* @param string $filename Filename of the destination image.
* @param string $mime_type The image mimetype.
*
* @return array|WP_Error Array containing new width, height, and file name on success. WP_Error object on failure.
*/
abstract public function stream( $mime_type = null );
/**
* Sets the quality of the image.
*
* @since 3.5.0
*
* @param int $quality Compression quality values from 0 (worst) to 100 (best).
*
* @return true|WP_Error True if set successfully. WP_Error on failure.
*/
public function set_quality( $quality ) {
if ( is_int( $quality ) && $quality >= 0 && $quality <= 100 ) {
$this->quality = $quality;
return true;
}
return new WP_Error( 'invalid_image_quality', __( 'Image quality must be a number between 0 and 100.' ) );
}
/**
* Gets the quality of the image.
*
* @since 3.5.0
*
* @return int Returns the image quality.
*/
public function get_quality() {
return $this->quality;
}
/**
* Test to see if an editor is usable.
*
* @since 3.5.0
*
* @param array $args Array of strings for extra information.
* @return bool True if editor is valid.
*/
public static function test( $args = array() ) {
return false;
}
}
这个抽象类定义了一些基本的属性,比如 $file
(图片文件路径)、$width
(图片宽度)、$height
(图片高度)、$quality
(图片质量)等等。
更重要的是,它定义了一系列抽象方法,比如 load()
、save()
、resize()
、crop()
、rotate()
、flip()
、stream()
等等。这些方法是图片编辑器必须实现的,用来完成各种图片编辑操作。
简单来说,WP_Image_Editor
就像一个接口规范,它规定了所有图片编辑器必须提供哪些功能。 这样,我们就可以用统一的方式来调用不同的图片编辑器,而不用关心它们底层的实现细节。
3. WP_Image_Editor_GD
:GD 库的实现
接下来,我们来看看 WP_Image_Editor_GD
类,它是 WP_Image_Editor
抽象类的具体实现,用来使用 GD 库进行图片编辑。 它的源码位于 wp-includes/class-wp-image-editor-gd.php
。
由于篇幅限制,我们只挑几个关键的方法来分析:
test()
方法: 这个方法用来判断服务器环境是否支持 GD 库。
/**
* Checks to see if current environment supports the GD library.
*
* @since 3.5.0
*
* @param array $args Not used.
* @return bool
*/
public static function test( $args = array() ) {
if ( ! function_exists( 'imagecreatetruecolor' ) ) {
return false;
}
if ( ! function_exists( 'imagecopyresampled' ) ) {
return false;
}
return true;
}
可以看到,它主要检查 imagecreatetruecolor()
和 imagecopyresampled()
这两个函数是否存在。如果这两个函数都存在,就说明 GD 库可用。
load()
方法: 这个方法用来加载图片文件,并创建 GD 图像资源。
/**
* Loads an image resource for editing.
*
* @since 3.5.0
*
* @return true|WP_Error True if loaded successfully. WP_Error on failure.
*/
public function load() {
if ( ! is_readable( $this->file ) ) {
return new WP_Error( 'image_file_missing', __( 'File does not exist.' ) );
}
$this->image = wp_load_image( $this->file );
if ( is_wp_error( $this->image ) ) {
return $this->image;
}
$this->size = @getimagesize( $this->file );
if ( ! $this->size ) {
return new WP_Error( 'invalid_image', __( 'Could not read image size.' ) );
}
$this->width = $this->size[0];
$this->height = $this->size[1];
return true;
}
这里用到了 wp_load_image()
函数来加载图片。 wp_load_image()
会根据图片类型,调用不同的 GD 函数来创建图像资源。 比如,如果是 JPEG 图片,就调用 imagecreatefromjpeg()
;如果是 PNG 图片,就调用 imagecreatefrompng()
。
resize()
方法: 这个方法用来缩放图片。
/**
* Resizes current image.
*
* @since 3.5.0
*
* @param int|null $max_w Optional. Maximum width.
* @param int|null $max_h Optional. Maximum height.
* @param bool $crop Optional. Whether to crop image to specified dimensions.
*
* @return true|WP_Error True if resize is successful. WP_Error on failure.
*/
public function resize( $max_w = null, $max_h = null, $crop = false ) {
if ( ( $this->width == $max_w ) && ( $this->height == $max_h ) ) {
return true;
}
$dims = image_resize_dimensions( $this->width, $this->height, $max_w, $max_h, $crop );
if ( ! $dims ) {
return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
}
list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
$new_image = imagecreatetruecolor( $dst_w, $dst_h );
imagealphablending( $new_image, false );
imagesavealpha( $new_image, true );
$result = imagecopyresampled( $new_image, $this->image, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h );
if ( ! $result ) {
return new WP_Error( 'image_resize_failed', __( 'Image resize failed.' ) );
}
imagedestroy( $this->image );
$this->image = $new_image;
$this->width = $dst_w;
$this->height = $dst_h;
return true;
}
这里用到了 image_resize_dimensions()
函数来计算缩放后的尺寸。 然后,它创建了一个新的 GD 图像资源,并使用 imagecopyresampled()
函数将原图复制到新图中,完成缩放操作。
save()
方法: 这个方法用来保存图片文件。
/**
* Saves current image to file.
*
* @since 3.5.0
*
* @param string|null $destfilename Optional. String containing the file name to save to. Defaults to the original file.
* @param string|null $mime_type Optional. Image mime type. Defaults to the mime type of the file.
*
* @return array|WP_Error Array containing new width, height, and file name on success. WP_Error object on failure.
*/
public function save( $destfilename = null, $mime_type = null ) {
if ( ! $destfilename ) {
$destfilename = $this->file;
}
if ( ! $mime_type ) {
$mime_type = $this->get_mime_type( $destfilename );
}
$quality = $this->get_quality();
if ( 'image/jpeg' == $mime_type ) {
if ( ! @imagejpeg( $this->image, $destfilename, $quality ) ) {
return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
}
} elseif ( 'image/png' == $mime_type ) {
// See https://wordpress.org/support/topic/gd-library-does-not-handle-transparency-well/.
if ( ! defined( 'PNG_FILTER_NONE' ) ) {
define( 'PNG_FILTER_NONE', 0 );
}
if ( ! @imagepng( $this->image, $destfilename, round( $quality / 10 ) ) ) {
return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
}
} elseif ( 'image/gif' == $mime_type ) {
if ( ! @imagegif( $this->image, $destfilename ) ) {
return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
}
} else {
return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
}
imagedestroy( $this->image );
/** This filter is documented in wp-includes/class-wp-image-editor.php */
return apply_filters( 'image_save_pre_send_to_editor', array(
'file' => $destfilename,
'width' => $this->width,
'height' => $this->height,
), $this->id );
}
这里根据图片类型,调用不同的 GD 函数来保存图片。 比如,如果是 JPEG 图片,就调用 imagejpeg()
;如果是 PNG 图片,就调用 imagepng()
。
总而言之,WP_Image_Editor_GD
类封装了 GD 库的各种函数,实现了 WP_Image_Editor
抽象类定义的接口。
4. WP_Image_Editor_Imagick
:Imagick 的实现
类似地,WP_Image_Editor_Imagick
类也是 WP_Image_Editor
抽象类的具体实现,用来使用 Imagick 库进行图片编辑。 它的源码位于 wp-includes/class-wp-image-editor-imagick.php
。
我们还是挑几个关键的方法来分析:
test()
方法: 这个方法用来判断服务器环境是否支持 Imagick 库。
/**
* Checks to see if current environment supports the Imagick library.
*
* @since 3.5.0
*
* @param array $args Not used.
* @return bool
*/
public static function test( $args = array() ) {
if ( ! extension_loaded( 'imagick' ) ) {
return false;
}
if ( ! class_exists( 'Imagick' ) ) {
return false;
}
$required_version = '6.2.4';
if ( defined( 'imagick::IMAGICK_EXTNUM' ) && imagick::IMAGICK_EXTNUM < 2000 ) {
return false;
}
if ( version_compare( phpversion( 'imagick' ), $required_version, '<' ) ) {
return false;
}
return true;
}
可以看到,它主要检查 imagick
扩展是否加载,以及 Imagick 的版本是否符合要求。
load()
方法: 这个方法用来加载图片文件,并创建 Imagick 对象。
/**
* Loads an image resource for editing.
*
* @since 3.5.0
*
* @return true|WP_Error True if loaded successfully. WP_Error on failure.
*/
public function load() {
if ( ! is_readable( $this->file ) ) {
return new WP_Error( 'image_file_missing', __( 'File does not exist.' ) );
}
try {
$this->image = new Imagick( $this->file );
if ( ! is_object( $this->image ) ) {
return new WP_Error( 'invalid_image', __( 'Could not read image.' ) );
}
if ( is_callable( array( $this->image, 'setImageOrientation' ) ) ) {
$this->image->setImageOrientation( imagick::ORIENTATION_TOPLEFT );
}
/*
* Strip profiles if not a whitelisted file type. Prevents image
* corruption issues.
*/
$strip = true;
$whitelisted_mimes = array(
'image/jpeg',
'image/png',
'image/gif',
'image/bmp',
'image/tiff',
);
$whitelisted_exts = array(
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'tif',
'tiff',
);
$file_ext = strtolower( pathinfo( $this->file, PATHINFO_EXTENSION ) );
$mime_type = $this->get_mime_type( $this->file );
if ( in_array( $mime_type, $whitelisted_mimes, true ) && in_array( $file_ext, $whitelisted_exts, true ) ) {
$strip = false;
}
if ( $strip ) {
$this->strip_image();
}
$this->unstrip();
$this->set_defaults();
$this->width = $this->image->getImageWidth();
$this->height = $this->image->getImageHeight();
return true;
} catch ( Exception $e ) {
return new WP_Error( 'invalid_image', $e->getMessage() );
}
}
这里直接使用 new Imagick()
来创建一个 Imagick 对象。
resize()
方法: 这个方法用来缩放图片。
/**
* Resizes current image.
*
* @since 3.5.0
*
* @param int|null $max_w Optional. Maximum width.
* @param int|null $max_h Optional. Maximum height.
* @param bool $crop Optional. Whether to crop image to specified dimensions.
*
* @return true|WP_Error True if resize is successful. WP_Error on failure.
*/
public function resize( $max_w = null, $max_h = null, $crop = false ) {
if ( ( $this->width == $max_w ) && ( $this->height == $max_h ) ) {
return true;
}
$dims = image_resize_dimensions( $this->width, $this->height, $max_w, $max_h, $crop );
if ( ! $dims ) {
return new WP_Error( 'error_getting_dimensions', __( 'Could not calculate resized image dimensions' ) );
}
list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
if ( $crop ) {
return $this->crop( $dst_x, $dst_y, $src_w, $src_h, $dst_w, $dst_h );
}
$thumb = $this->image->clone();
/**
* Note: setResourceLimit() is only available since Imagick 6.3.7.
* See: https://bugs.php.net/bug.php?id=43777
*/
if ( is_callable( array( $thumb, 'setResourceLimit' ) ) ) {
$thumb->setResourceLimit( Imagick::RESOURCETYPE_MEMORY, $max_w * $max_h * 8 );
}
$thumb->setImageInterpolateMethod( Imagick::INTERPOLATE_BICUBIC );
$thumb->resizeImage( $dst_w, $dst_h, Imagick::FILTER_LANCZOS, 1 );
$thumb->setImagePage( 0, 0, 0, 0 );
$this->image->clear();
$this->image->destroy();
$this->image = $thumb;
$this->width = $dst_w;
$this->height = $dst_h;
return true;
}
这里用到了 Imagick::resizeImage()
函数来缩放图片。
save()
方法: 这个方法用来保存图片文件。
/**
* Saves current image to file.
*
* @since 3.5.0
*
* @param string|null $destfilename Optional. String containing the file name to save to. Defaults to the original file.
* @param string|null $mime_type Optional. Image mime type. Defaults to the mime type of the file.
*
* @return array|WP_Error Array containing new width, height, and file name on success. WP_Error object on failure.
*/
public function save( $destfilename = null, $mime_type = null ) {
if ( ! $destfilename ) {
$destfilename = $this->file;
}
if ( ! $mime_type ) {
$mime_type = $this->get_mime_type( $destfilename );
}
$quality = $this->get_quality();
try {
if ( ! empty( $quality ) ) {
$this->image->setImageCompressionQuality( $quality );
}
/*
* Prevent Imagick from writing EXIF data into PNG files.
*
* See https://wordpress.org/support/topic/image-rotation-issue-with-wp-4-6.
*/
if ( 'image/png' === $mime_type ) {
$this->image->stripImage();
}
if ( 'image/jpeg' === $mime_type && defined( 'WP_IMAGE_EDIT_JPEG_QUALITY' ) ) {
$this->image->setImageCompressionQuality( WP_IMAGE_EDIT_JPEG_QUALITY );
}
if ( ! $this->image->writeImage( $destfilename ) ) {
return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
}
$this->set_defaults();
/** This filter is documented in wp-includes/class-wp-image-editor.php */
return apply_filters( 'image_save_pre_send_to_editor', array(
'file' => $destfilename,
'width' => $this->width,
'height' => $this->height,
), $this->id );
} catch ( Exception $e ) {
return new WP_Error( 'image_save_error', $e->getMessage() );
}
}
这里使用 Imagick::writeImage()
函数来保存图片。
同样,WP_Image_Editor_Imagick
类封装了 Imagick 库的各种函数,实现了 WP_Image_Editor
抽象类定义的接口。
5. 总结:wp_get_image_editor()
的价值
通过以上的源码分析,我们可以看到 wp_get_image_editor()
函数的价值所在:
- 屏蔽了底层图片处理库的差异: 无论是 GD 还是 Imagick,它们都有自己的 API 和用法。
wp_get_image_editor()
让我们不用关心这些差异,只需要调用统一的WP_Image_Editor
接口即可。 - 提高了代码的可维护性和可扩展性: 如果我们需要添加新的图片编辑器(比如使用一个新的图片处理库),只需要实现
WP_Image_Editor
抽象类,并将其添加到$implementations
数组中即可。 - 简化了图片处理流程: 我们可以用简单的几行代码,就可以完成复杂的图片编辑操作。
为了更清晰地展示 wp_get_image_editor()
的作用,我们用一个表格来总结一下:
| 函数/类名 | 作用