diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index 0430dbefbf..52b403d180 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -306,6 +306,12 @@ function wp_stream_image( $image, $mime_type, $attachment_id ) { case 'image/gif': header( 'Content-Type: image/gif' ); return imagegif( $image ); + case 'image/webp': + if ( function_exists( 'imagewebp' ) ) { + header( 'Content-Type: image/webp' ); + return imagewebp( $image, null, 90 ); + } + return false; default: return false; } @@ -391,6 +397,11 @@ function wp_save_image_file( $filename, $image, $mime_type, $post_id ) { return imagepng( $image, $filename ); case 'image/gif': return imagegif( $image, $filename ); + case 'image/webp': + if ( function_exists( 'imagewebp' ) ) { + return imagewebp( $image, $filename ); + } + return false; default: return false; } diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index 673c1dff01..142c245f89 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -517,6 +517,9 @@ function wp_generate_attachment_metadata( $attachment_id, $file ) { case 'image/png': $ext = '.png'; break; + case 'image/webp': + $ext = '.webp'; + break; } $basename = str_replace( '.', '-', wp_basename( $file ) ) . '-image' . $ext; $uploaded = wp_upload_bits( $basename, '', $metadata['image']['data'] ); @@ -913,7 +916,7 @@ function file_is_valid_image( $path ) { * @return bool True if suitable, false if not suitable. */ function file_is_displayable_image( $path ) { - $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO ); + $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP ); // phpcs:ignore PHPCompatibility.Constants.NewConstants.imagetype_webpFound $info = wp_getimagesize( $path ); if ( empty( $info ) ) { @@ -963,6 +966,12 @@ function load_image_to_edit( $attachment_id, $mime_type, $size = 'full' ) { case 'image/gif': $image = imagecreatefromgif( $filepath ); break; + case 'image/webp': + $image = false; + if ( function_exists( 'imagecreatefromwebp' ) ) { + $image = imagecreatefromwebp( $filepath ); + } + break; default: $image = false; break; diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 14a8d99b49..bdfb65d103 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -993,7 +993,7 @@ function wp_media_upload_handler() { function media_sideload_image( $file, $post_id = 0, $desc = null, $return = 'html' ) { if ( ! empty( $file ) ) { - $allowed_extensions = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif' ); + $allowed_extensions = array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' ); /** * Filters the list of allowed file extensions when sideloading an image from a URL. diff --git a/src/wp-admin/includes/schema.php b/src/wp-admin/includes/schema.php index a59c7c36ce..6f528f2288 100644 --- a/src/wp-admin/includes/schema.php +++ b/src/wp-admin/includes/schema.php @@ -1215,6 +1215,7 @@ We hope you enjoy your new site. Thanks! 'jpeg', 'png', 'gif', + 'webp', // Video. 'mov', 'avi', diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php index ed0a7be279..42a8929ff8 100644 --- a/src/wp-includes/class-wp-image-editor-gd.php +++ b/src/wp-includes/class-wp-image-editor-gd.php @@ -69,6 +69,8 @@ class WP_Image_Editor_GD extends WP_Image_Editor { return ( $image_types & IMG_PNG ) != 0; case 'image/gif': return ( $image_types & IMG_GIF ) != 0; + case 'image/webp': + return ( $image_types & IMG_WEBP ) != 0; // phpcs:ignore PHPCompatibility.Constants.NewConstants.img_webpFound } return false; @@ -99,7 +101,15 @@ class WP_Image_Editor_GD extends WP_Image_Editor { return new WP_Error( 'error_loading_image', __( 'File doesn’t exist?' ), $this->file ); } - $this->image = @imagecreatefromstring( $file_contents ); + // WebP may not work with imagecreatefromstring(). + if ( + function_exists( 'imagecreatefromwebp' ) && + ( 'image/webp' === wp_get_image_mime( $this->file ) ) + ) { + $this->image = @imagecreatefromwebp( $this->file ); + } else { + $this->image = @imagecreatefromstring( $file_contents ); + } if ( ! is_gd_image( $this->image ) ) { return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file ); @@ -459,6 +469,10 @@ class WP_Image_Editor_GD extends WP_Image_Editor { if ( ! $this->make_image( $filename, 'imagejpeg', array( $image, $filename, $this->get_quality() ) ) ) { return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); } + } elseif ( 'image/webp' == $mime_type ) { + if ( ! function_exists( 'imagewebp' ) || ! $this->make_image( $filename, 'imagewebp', array( $image, $filename, $this->get_quality() ) ) ) { + return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); + } } else { return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) ); } @@ -502,6 +516,12 @@ class WP_Image_Editor_GD extends WP_Image_Editor { case 'image/gif': header( 'Content-Type: image/gif' ); return imagegif( $this->image ); + case 'image/webp': + if ( function_exists( 'imagewebp' ) ) { + header( 'Content-Type: image/webp' ); + return imagewebp( $this->image, null, $this->get_quality() ); + } + // Fall back to the default if webp isn't supported. default: header( 'Content-Type: image/jpeg' ); return imagejpeg( $this->image, null, $this->get_quality() ); diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index e4f1ea1b16..36a344b15b 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -197,19 +197,30 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { } try { - if ( 'image/jpeg' === $this->mime_type ) { - $this->image->setImageCompressionQuality( $quality ); - $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); - } else { - $this->image->setImageCompressionQuality( $quality ); + switch ( $this->mime_type ) { + case 'image/jpeg': + $this->image->setImageCompressionQuality( $quality ); + $this->image->setImageCompression( imagick::COMPRESSION_JPEG ); + break; + case 'image/webp': + if ( _wp_webp_is_lossy( $this->file ) ) { + $this->image->setImageCompressionQuality( $quality ); + } else { + // Use WebP lossless settings. + $this->image->setImageCompressionQuality( 100 ); + $this->image->setOption( 'webp:lossless', 'true' ); + } + break; + default: + $this->image->setImageCompressionQuality( $quality ); } } catch ( Exception $e ) { return new WP_Error( 'image_quality_error', $e->getMessage() ); } - return true; } + /** * Sets or updates current image size. * diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index d9ccf6b6ba..b365f45715 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -1141,7 +1141,7 @@ final class WP_Theme implements ArrayAccess { return false; } - foreach ( array( 'png', 'gif', 'jpg', 'jpeg' ) as $ext ) { + foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp' ) as $ext ) { if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) { $this->cache_add( 'screenshot', 'screenshot.' . $ext ); if ( 'relative' === $uri ) { diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 7f6c59f03c..59e5700439 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -370,3 +370,11 @@ if ( ! function_exists( 'is_iterable' ) ) { return ( is_array( $var ) || $var instanceof Traversable ); } } + +// WebP constants may not be defined, even in cases where the format is supported. +if ( ! defined( 'IMAGETYPE_WEBP' ) ) { + define( 'IMAGETYPE_WEBP', 18 ); +} +if ( ! defined( 'IMG_WEBP' ) ) { + define( 'IMG_WEBP', IMAGETYPE_WEBP ); // phpcs:ignore PHPCompatibility.Constants.NewConstants.imagetype_webpFound +} diff --git a/src/wp-includes/customize/class-wp-customize-media-control.php b/src/wp-includes/customize/class-wp-customize-media-control.php index d327a4fa38..22bf75dad5 100644 --- a/src/wp-includes/customize/class-wp-customize-media-control.php +++ b/src/wp-includes/customize/class-wp-customize-media-control.php @@ -91,7 +91,7 @@ class WP_Customize_Media_Control extends WP_Customize_Control { // Fake an attachment model - needs all fields used by template. // Note that the default value must be a URL, NOT an attachment ID. $ext = substr( $this->setting->default, -3 ); - $type = in_array( $ext, array( 'jpg', 'png', 'gif', 'bmp' ), true ) ? 'image' : 'document'; + $type = in_array( $ext, array( 'jpg', 'png', 'gif', 'bmp', 'webp' ), true ) ? 'image' : 'document'; $default_attachment = array( 'id' => 1, diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 72a23cd836..2c5f24bde0 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -3340,6 +3340,8 @@ function gd_edit_image_support($mime_type) { return (imagetypes() & IMG_PNG) != 0; case 'image/gif': return (imagetypes() & IMG_GIF) != 0; + case 'image/webp': + return (imagetypes() & IMG_WEBP) != 0; // phpcs:ignore PHPCompatibility.Constants.NewConstants.img_webpFound } } else { switch( $mime_type ) { @@ -3349,6 +3351,8 @@ function gd_edit_image_support($mime_type) { return function_exists('imagecreatefrompng'); case 'image/gif': return function_exists('imagecreatefromgif'); + case 'image/webp': + return function_exists('imagecreatefromwebp'); } } return false; diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index b716c6bc1f..8dda8c738d 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3318,7 +3318,7 @@ function translate_smiley( $matches ) { $matches = array(); $ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false; - $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png' ); + $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); // Don't convert smilies that aren't images - they're probably emoji. if ( ! in_array( $ext, $image_exts, true ) ) { diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 56d5fd9f43..3bb257ab8b 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -2886,6 +2886,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { 'image/gif' => 'gif', 'image/bmp' => 'bmp', 'image/tiff' => 'tif', + 'image/webp' => 'webp', ) ); @@ -3063,6 +3064,35 @@ function wp_get_image_mime( $file ) { } else { $mime = false; } + + if ( false !== $mime ) { + return $mime; + } + + $handle = fopen( $file, 'rb' ); + if ( false === $handle ) { + return false; + } + + $magic = fread( $handle, 12 ); + if ( false === $magic ) { + return false; + } + + // Add WebP fallback detection when image library doesn't support WebP. + // Note: detection values come from LibWebP, see + // https://github.com/webmproject/libwebp/blob/master/imageio/image_dec.c#L30 + $magic = bin2hex( $magic ); + if ( + // RIFF. + ( 0 === strpos( $magic, '52494646' ) ) && + // WEBP. + ( 16 === strpos( $magic, '57454250' ) ) + ) { + $mime = 'image/webp'; + } + + fclose( $handle ); } catch ( Exception $e ) { $mime = false; } @@ -3101,6 +3131,7 @@ function wp_get_mime_types() { 'png' => 'image/png', 'bmp' => 'image/bmp', 'tiff|tif' => 'image/tiff', + 'webp' => 'image/webp', 'ico' => 'image/x-icon', 'heic' => 'image/heic', // Video formats. @@ -3222,7 +3253,7 @@ function wp_get_ext_types() { return apply_filters( 'ext2type', array( - 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic' ), + 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp' ), 'audio' => array( 'aac', 'ac3', 'aif', 'aiff', 'flac', 'm3a', 'm4a', 'm4b', 'mka', 'mp1', 'mp2', 'mp3', 'ogg', 'oga', 'ram', 'wav', 'wma' ), 'video' => array( '3g2', '3gp', '3gpp', 'asf', 'avi', 'divx', 'dv', 'flv', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mpv', 'ogm', 'ogv', 'qt', 'rm', 'vob', 'wmv' ), 'document' => array( 'doc', 'docx', 'docm', 'dotm', 'odt', 'pages', 'pdf', 'xps', 'oxps', 'rtf', 'wp', 'wpd', 'psd', 'xcf' ), diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index f5b6a39968..82d1b32627 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4980,6 +4980,7 @@ function wp_show_heic_upload_error( $plupload_settings ) { * Allows PHP's getimagesize() to be debuggable when necessary. * * @since 5.7.0 + * @since 5.8.0 Added support for WebP images. * * @param string $filename The file path. * @param array $image_info Optional. Extended image information (passed by reference). @@ -4994,9 +4995,9 @@ function wp_getimagesize( $filename, array &$image_info = null ) { defined( 'WP_DEBUG' ) && WP_DEBUG ) { if ( 2 === func_num_args() ) { - return getimagesize( $filename, $image_info ); + return _wp_get_image_size( $filename, $image_info ); } else { - return getimagesize( $filename ); + return _wp_get_image_size( $filename ); } } @@ -5011,9 +5012,133 @@ function wp_getimagesize( $filename, array &$image_info = null ) { */ if ( 2 === func_num_args() ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors - return @getimagesize( $filename, $image_info ); + return @_wp_get_image_size( $filename, $image_info ); } else { // phpcs:ignore WordPress.PHP.NoSilencedErrors - return @getimagesize( $filename ); + return @_wp_get_image_size( $filename ); } } + +/** + * Extracts meta information about a webp file: width, height and type. + * + * @since 5.8.0 + * + * @param [type] $filename Path to a WebP file. + * @return array $webp_info { + * An array of WebP image information. + * + * @type array $size { + * @type int $width Image width. + * @type int $height Image height. + * @type bool $type The WebP type: one of 'lossy', 'lossless' or 'animated-alpha'. + * } + */ +function wp_get_webp_info( $filename ) { + $width = false; + $height = false; + $type = false; + if ( ! 'image/webp' === wp_get_image_mime( $filename ) ) { + return compact( 'width', 'height', 'type' ); + } + try { + $handle = fopen( $filename, 'rb' ); + if ( $handle ) { + $magic = fread( $handle, 40 ); + fclose( $handle ); + + // Make sure we got enough bytes. + if ( strlen( $magic ) < 40 ) { + return compact( 'width', 'height', 'type' ); + } + + // The headers are a little different for each of the three formats. + // Header values based on WebP docs, see https://developers.google.com/speed/webp/docs/riff_container. + switch ( substr( $magic, 12, 4 ) ) { + // Lossy WebP. + case 'VP8 ': + $parts = unpack( 'v2', substr( $magic, 26, 4 ) ); + $width = (int) ( $parts[1] & 0x3FFF ); + $height = (int) ( $parts[2] & 0x3FFF ); + $type = 'lossy'; + break; + // Lossless WebP. + case 'VP8L': + $parts = unpack( 'C4', substr( $magic, 21, 4 ) ); + $width = (int) ( $parts[1] | ( ( $parts[2] & 0x3F ) << 8 ) ) + 1; + $height = (int) ( ( ( $parts[2] & 0xC0 ) >> 6 ) | ( $parts[3] << 2 ) | ( ( $parts[4] & 0x03 ) << 10 ) ) + 1; + $type = 'lossless'; + break; + // Animated/alpha WebP. + case 'VP8X': + // Pad 24-bit int. + $width = unpack( 'V', substr( $magic, 24, 3 ) . "\x00" ); + $width = (int) ( $width[1] & 0xFFFFFF ) + 1; + // Pad 24-bit int. + $height = unpack( 'V', substr( $magic, 27, 3 ) . "\x00" ); + $height = (int) ( $height[1] & 0xFFFFFF ) + 1; + $type = 'animated-alpha'; + break; + } + } + } catch ( Exception $e ) { + } + return compact( 'width', 'height', 'type' ); +} + +/** + * Determines if a passed image is a lossy WebP image. + * + * @since 5.8.0 + * + * @param string $filename The file path. + * @return bool Whether the file is a lossy WebP file. + */ +function _wp_webp_is_lossy( $filename ) { + $webp_info = wp_get_webp_info( $filename ); + $type = $webp_info['type']; + return $type && 'lossy' === $type; +} + +/** + * Gets the image size, with support for WebP images. + * + * @since 5.8.0 + * @access private + * + * @param string $filename The file path. + * @param array $imageinfo Extended image information, passed by reference. + * @return array|false Array of image information or false on failure. + */ +function _wp_get_image_size( $filename, &$imageinfo = array() ) { + // Try getimagesize() first. + $info = getimagesize( $filename, $imageinfo ); + if ( false !== $info ) { + return $info; + } + // For PHP versions that don't support WebP images, extract the image + // size info from the file headers. + if ( 'image/webp' === wp_get_image_mime( $filename ) ) { + $webp_info = wp_get_webp_info( $filename ); + $width = $webp_info['width']; + $height = $webp_info['height']; + + // Mimic the native return format. + if ( $width && $height ) { + return array( + $width, + $height, + IMAGETYPE_WEBP, // phpcs:ignore PHPCompatibility.Constants.NewConstants.imagetype_webpFound + sprintf( + 'width="%d" height="%d"', + $width, + $height + ), + 'mime' => 'image/webp', + ); + } + } + + // The image could not be parsed. + return false; +} diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index abf6f1df46..56cd4a8cd5 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -6539,7 +6539,7 @@ function wp_attachment_is( $type, $post = null ) { switch ( $type ) { case 'image': - $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png' ); + $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); return in_array( $ext, $image_exts, true ); case 'audio': diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index ea4ae947cc..007cee774b 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -438,7 +438,7 @@ class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { ); } - $supported_types = array( 'image/jpeg', 'image/png', 'image/gif' ); + $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ); $mime_type = get_post_mime_type( $attachment_id ); if ( ! in_array( $mime_type, $supported_types, true ) ) { return new WP_Error( diff --git a/tests/phpunit/data/images/test-image.webp b/tests/phpunit/data/images/test-image.webp new file mode 100644 index 0000000000..37ab153807 Binary files /dev/null and b/tests/phpunit/data/images/test-image.webp differ diff --git a/tests/phpunit/data/images/webp-animated.webp b/tests/phpunit/data/images/webp-animated.webp new file mode 100644 index 0000000000..c60d334a82 Binary files /dev/null and b/tests/phpunit/data/images/webp-animated.webp differ diff --git a/tests/phpunit/data/images/webp-lossless.webp b/tests/phpunit/data/images/webp-lossless.webp new file mode 100644 index 0000000000..7a3a06e0b4 Binary files /dev/null and b/tests/phpunit/data/images/webp-lossless.webp differ diff --git a/tests/phpunit/data/images/webp-lossy.webp b/tests/phpunit/data/images/webp-lossy.webp new file mode 100644 index 0000000000..c8b0e25391 Binary files /dev/null and b/tests/phpunit/data/images/webp-lossy.webp differ diff --git a/tests/phpunit/data/images/webp-transparent.webp b/tests/phpunit/data/images/webp-transparent.webp new file mode 100644 index 0000000000..c4b24a08be Binary files /dev/null and b/tests/phpunit/data/images/webp-transparent.webp differ diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index f485d3b4ec..a088db940e 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1225,6 +1225,29 @@ class Tests_Functions extends WP_UnitTestCase { $this->assertSame( $expected, wp_get_image_mime( $file ) ); } + /** + * @ticket 35725 + * @dataProvider data_wp_getimagesize + */ + public function test_wp_getimagesize( $file, $expected ) { + if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) { + $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); + } + + $result = wp_getimagesize( $file ); + + // The getimagesize() function varies in its response, so + // let's restrict comparison to expected keys only. + if ( is_array( $expected ) ) { + foreach ( $expected as $k => $v ) { + $this->assertEquals( true, isset( $result[ $k ] ) ); + $this->assertEquals( $expected[ $k ], $result[ $k ] ); + } + } else { + $this->assertEquals( $expected, $result ); + } + } + /** * @ticket 39550 * @dataProvider _wp_check_filetype_and_ext_data @@ -1313,6 +1336,129 @@ class Tests_Functions extends WP_UnitTestCase { DIR_TESTDATA . '/images/test-image-mime-jpg.png', 'image/jpeg', ), + // Animated WebP. + array( + DIR_TESTDATA . '/images/webp-animated.webp', + 'image/webp', + ), + // Lossless WebP. + array( + DIR_TESTDATA . '/images/webp-lossless.webp', + 'image/webp', + ), + // Lossy WebP. + array( + DIR_TESTDATA . '/images/webp-lossy.webp', + 'image/webp', + ), + // Transparent WebP. + array( + DIR_TESTDATA . '/images/webp-transparent.webp', + 'image/webp', + ), + // Not an image. + array( + DIR_TESTDATA . '/uploads/dashicons.woff', + false, + ), + ); + + return $data; + } + + /** + * Data profider for test_wp_getimagesize(); + */ + public function data_wp_getimagesize() { + $data = array( + // Standard JPEG. + array( + DIR_TESTDATA . '/images/test-image.jpg', + array( + 50, + 50, + IMAGETYPE_JPEG, + 'width="50" height="50"', + 'mime' => 'image/jpeg', + ), + ), + // Standard GIF. + array( + DIR_TESTDATA . '/images/test-image.gif', + array( + 50, + 50, + IMAGETYPE_GIF, + 'width="50" height="50"', + 'mime' => 'image/gif', + ), + ), + // Standard PNG. + array( + DIR_TESTDATA . '/images/test-image.png', + array( + 50, + 50, + IMAGETYPE_PNG, + 'width="50" height="50"', + 'mime' => 'image/png', + ), + ), + // Image with wrong extension. + array( + DIR_TESTDATA . '/images/test-image-mime-jpg.png', + array( + 50, + 50, + IMAGETYPE_JPEG, + 'width="50" height="50"', + 'mime' => 'image/jpeg', + ), + ), + // Animated WebP. + array( + DIR_TESTDATA . '/images/webp-animated.webp', + array( + 100, + 100, + IMAGETYPE_WEBP, + 'width="100" height="100"', + 'mime' => 'image/webp', + ), + ), + // Lossless WebP. + array( + DIR_TESTDATA . '/images/webp-lossless.webp', + array( + 1200, + 675, + IMAGETYPE_WEBP, + 'width="1200" height="675"', + 'mime' => 'image/webp', + ), + ), + // Lossy WebP. + array( + DIR_TESTDATA . '/images/webp-lossy.webp', + array( + 1200, + 675, + IMAGETYPE_WEBP, + 'width="1200" height="675"', + 'mime' => 'image/webp', + ), + ), + // Transparent WebP. + array( + DIR_TESTDATA . '/images/webp-transparent.webp', + array( + 1200, + 675, + IMAGETYPE_WEBP, + 'width="1200" height="675"', + 'mime' => 'image/webp', + ), + ), // Not an image. array( DIR_TESTDATA . '/uploads/dashicons.woff', diff --git a/tests/phpunit/tests/image/editor.php b/tests/phpunit/tests/image/editor.php index 47ee30a658..dce95ee836 100644 --- a/tests/phpunit/tests/image/editor.php +++ b/tests/phpunit/tests/image/editor.php @@ -195,4 +195,84 @@ class Tests_Image_Editor extends WP_Image_UnitTestCase { $this->assertSame( '100x50', $editor->get_suffix() ); } + + /** + * Test wp_get_webp_info. + * + * @ticket 35725 + * @dataProvider _test_wp_get_webp_info + * + */ + public function test_wp_get_webp_info( $file, $expected ) { + $editor = wp_get_image_editor( $file ); + if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/webp' ) ) { + $this->markTestSkipped( sprintf( 'Skipping test: no WebP support in the editor engine %s on this system.', $this->editor_engine ) ); + } else { + $file_data = wp_get_webp_info( $file ); + $this->assertSame( $file_data, $expected ); + } + } + + /** + * Data provider for test_wp_get_webp_info(); + */ + public function _test_wp_get_webp_info() { + return array( + // Standard JPEG. + array( + DIR_TESTDATA . '/images/test-image.jpg', + array( + 'width' => false, + 'height' => false, + 'type' => false, + ), + ), + // Standard GIF. + array( + DIR_TESTDATA . '/images/test-image.gif', + array( + 'width' => false, + 'height' => false, + 'type' => false, + ), + ), + // Animated WebP. + array( + DIR_TESTDATA . '/images/webp-animated.webp', + array( + 'width' => 100, + 'height' => 100, + 'type' => 'animated-alpha', + ), + ), + // Lossless WebP. + array( + DIR_TESTDATA . '/images/webp-lossless.webp', + array( + 'width' => 1200, + 'height' => 675, + 'type' => 'lossless', + ), + ), + // Lossy WebP. + array( + DIR_TESTDATA . '/images/webp-lossy.webp', + array( + 'width' => 1200, + 'height' => 675, + 'type' => 'lossy', + ), + ), + // Transparent WebP. + array( + DIR_TESTDATA . '/images/webp-transparent.webp', + array( + 'width' => 1200, + 'height' => 675, + 'type' => 'animated-alpha', + ), + ), + ); + } + } diff --git a/tests/phpunit/tests/image/functions.php b/tests/phpunit/tests/image/functions.php index 1694c1e7f1..0658e45117 100644 --- a/tests/phpunit/tests/image/functions.php +++ b/tests/phpunit/tests/image/functions.php @@ -59,6 +59,10 @@ class Tests_Image_Functions extends WP_UnitTestCase { 'test-image.psd', 'test-image-zip.tiff', 'test-image.jpg', + 'webp-animated.webp', + 'webp-lossless.webp', + 'webp-lossy.webp', + 'webp-transparent.webp', ); // IMAGETYPE_ICO is only defined in PHP 5.3+. @@ -92,6 +96,21 @@ class Tests_Image_Functions extends WP_UnitTestCase { 'test-image.jpg', ); + // Add WebP images if the image editor supports them. + $file = DIR_TESTDATA . '/images/test-image.webp'; + $editor = wp_get_image_editor( $file ); + if ( ( ! is_wp_error( $editor ) ) && $editor->supports_mime_type( 'image/webp' ) ) { + $files = array_merge( + $files, + array( + 'webp-animated.webp', + 'webp-lossless.webp', + 'webp-lossy.webp', + 'webp-transparent.webp', + ) + ); + } + // IMAGETYPE_ICO is only defined in PHP 5.3+. if ( defined( 'IMAGETYPE_ICO' ) ) { $files[] = 'test-image.ico'; @@ -174,6 +193,11 @@ class Tests_Image_Functions extends WP_UnitTestCase { 'image/png', ); + // Include WebP in tests when platform supports it. + if ( function_exists( 'imagewebp' ) ) { + array_push( $mime_types, 'image/webp' ); + } + // Test each image editor engine. foreach ( $classes as $class ) { $img = new $class( DIR_TESTDATA . '/images/canola.jpg' ); @@ -270,7 +294,8 @@ class Tests_Image_Functions extends WP_UnitTestCase { 'jpe' => 'image/jpeg', 'gif' => 'image/gif', 'png' => 'image/png', - 'unk' => 'image/jpeg', // Default, unknown. + 'webp' => 'image/webp', + 'unk' => 'image/jpeg', // Default, unknown. ); // Test each image editor engine. diff --git a/tests/phpunit/tests/image/resize.php b/tests/phpunit/tests/image/resize.php index 3e3255e6a5..517ab45389 100644 --- a/tests/phpunit/tests/image/resize.php +++ b/tests/phpunit/tests/image/resize.php @@ -64,6 +64,24 @@ abstract class WP_Tests_Image_Resize_UnitTestCase extends WP_Image_UnitTestCase unlink( $image ); } + function test_resize_webp() { + $file = DIR_TESTDATA . '/images/test-image.webp'; + $editor = wp_get_image_editor( $file ); + + // Check if the editor supports the webp mime type. + if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/webp' ) ) { + $this->markTestSkipped( sprintf( 'Skipping test: no WebP support in the editor engine %s on this system.', $this->editor_engine ) ); + } else { + $image = $this->resize_helper( $file, 25, 25 ); + $this->assertSame( 'test-image-25x25.webp', wp_basename( $image ) ); + list($w, $h, $type) = wp_getimagesize( $image ); + $this->assertSame( 25, $w ); + $this->assertSame( 25, $h ); + $this->assertSame( IMAGETYPE_WEBP, $type ); + unlink( $image ); + } + } + function test_resize_larger() { // image_resize() should refuse to make an image larger. $image = $this->resize_helper( DIR_TESTDATA . '/images/test-image.jpg', 100, 100 );