diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ccb04303218ae..840bdad8fe977 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -59,6 +59,7 @@ /src/wp-includes/class-requests\.php /src/wp-includes/class-simplepie\.php /src/wp-includes/class-snoopy\.php + /src/wp-includes/class-avif-info\.php /src/wp-includes/deprecated\.php /src/wp-includes/ms-deprecated\.php /src/wp-includes/pluggable-deprecated\.php diff --git a/src/js/_enqueues/vendor/plupload/handlers.js b/src/js/_enqueues/vendor/plupload/handlers.js index b82a6e88479ad..71e248fb5ed9f 100644 --- a/src/js/_enqueues/vendor/plupload/handlers.js +++ b/src/js/_enqueues/vendor/plupload/handlers.js @@ -608,6 +608,11 @@ jQuery( document ).ready( function( $ ) { wpQueueError( pluploadL10n.noneditable_image ); up.removeFile( file ); return; + } else if ( file.type === 'image/avif' && up.settings.avif_upload_error ) { + // Disallow uploading of AVIF images if the server cannot edit them. + wpQueueError( pluploadL10n.noneditable_image ); + up.removeFile( file ); + return; } fileQueued( file ); diff --git a/src/js/_enqueues/vendor/plupload/wp-plupload.js b/src/js/_enqueues/vendor/plupload/wp-plupload.js index 0fdebf77d1858..c0eb570657bf4 100644 --- a/src/js/_enqueues/vendor/plupload/wp-plupload.js +++ b/src/js/_enqueues/vendor/plupload/wp-plupload.js @@ -363,6 +363,11 @@ window.wp = window.wp || {}; error( pluploadL10n.noneditable_image, {}, file, 'no-retry' ); up.removeFile( file ); return; + } else if ( file.type === 'image/avif' && up.settings.avif_upload_error ) { + // Disallow uploading of AVIF images if the server cannot edit them. + error( pluploadL10n.noneditable_image, {}, file, 'no-retry' ); + up.removeFile( file ); + return; } // Generate attributes for a new `Attachment` model. diff --git a/src/js/_enqueues/vendor/thickbox/thickbox.js b/src/js/_enqueues/vendor/thickbox/thickbox.js index 5470467a1e0a8..e8b95677c1ad4 100644 --- a/src/js/_enqueues/vendor/thickbox/thickbox.js +++ b/src/js/_enqueues/vendor/thickbox/thickbox.js @@ -76,7 +76,7 @@ function tb_show(caption, url, imageGroup) {//function called when the user clic baseURL = url; } - var urlString = /\.jpg$|\.jpeg$|\.png$|\.gif$|\.bmp$|\.webp$/; + var urlString = /\.jpg$|\.jpeg$|\.png$|\.gif$|\.bmp$|\.webp$|\.avif$/; var urlType = baseURL.toLowerCase().match(urlString); if(urlType == '.jpg' || @@ -84,7 +84,8 @@ function tb_show(caption, url, imageGroup) {//function called when the user clic urlType == '.png' || urlType == '.gif' || urlType == '.bmp' || - urlType == '.webp' + urlType == '.webp' || + urlType == '.avif' ){//code to show images TB_PrevCaption = ""; diff --git a/src/js/media/controllers/library.js b/src/js/media/controllers/library.js index 2acc89a58692e..126ce8d7837fb 100644 --- a/src/js/media/controllers/library.js +++ b/src/js/media/controllers/library.js @@ -196,7 +196,7 @@ Library = wp.media.controller.State.extend(/** @lends wp.media.controller.Librar isImageAttachment: function( attachment ) { // If uploading, we know the filename but not the mime type. if ( attachment.get('uploading') ) { - return /\.(jpe?g|png|gif|webp)$/i.test( attachment.get('filename') ); + return /\.(jpe?g|png|gif|webp|avif)$/i.test( attachment.get('filename') ); } return attachment.get('type') === 'image'; diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index 739b09f9a1fca..2d150e691c293 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -390,6 +390,12 @@ function wp_stream_image( $image, $mime_type, $attachment_id ) { return imagewebp( $image, null, 90 ); } return false; + case 'image/avif': + if ( function_exists( 'imageavif' ) ) { + header( 'Content-Type: image/avif' ); + return imageavif( $image, null, 90 ); + } + return false; default: return false; } @@ -494,6 +500,11 @@ function wp_save_image_file( $filename, $image, $mime_type, $post_id ) { return imagewebp( $image, $filename ); } return false; + case 'image/avif': + if ( function_exists( 'imageavif' ) ) { + return imageavif( $image, $filename ); + } + return false; default: return false; } diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php index d60ec8508baf5..0f4ba818e6535 100644 --- a/src/wp-admin/includes/image.php +++ b/src/wp-admin/includes/image.php @@ -1006,7 +1006,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, IMAGETYPE_WEBP ); + $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP, IMAGETYPE_AVIF ); $info = wp_getimagesize( $path ); if ( empty( $info ) ) { diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index 57df2bcbff0c3..193e67f7dd641 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -2198,6 +2198,11 @@ function media_upload_form( $errors = null ) { $plupload_init['webp_upload_error'] = true; } + // Check if AVIF images can be edited. + if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) { + $plupload_init['avif_upload_error'] = true; + } + /** * Filters the default Plupload settings. * diff --git a/src/wp-admin/includes/schema.php b/src/wp-admin/includes/schema.php index 20648d7ddee94..63655ccf174ca 100644 --- a/src/wp-admin/includes/schema.php +++ b/src/wp-admin/includes/schema.php @@ -1250,6 +1250,7 @@ function populate_network_meta( $network_id, array $meta = array() ) { 'png', 'gif', 'webp', + 'avif', // Video. 'mov', 'avi', diff --git a/src/wp-includes/class-avif-info.php b/src/wp-includes/class-avif-info.php new file mode 100644 index 0000000000000..93280b3806dad --- /dev/null +++ b/src/wp-includes/class-avif-info.php @@ -0,0 +1,781 @@ += 2^31 on 32-bit systems. + // See https://www.php.net/manual/en/function.unpack.php#106041 + return unpack( 'N', $input ) [1]; + } +} + +/** + * Reads bytes and advances the stream position by the same count. + * + * @param stream $handle Bytes will be read from this resource. + * @param int $num_bytes Number of bytes read. Must be greater than 0. + * @return binary string|false The raw bytes or false on failure. + */ +function read( $handle, $num_bytes ) { + $data = fread( $handle, $num_bytes ); + return ( $data !== false && strlen( $data ) >= $num_bytes ) ? $data : false; +} + +/** + * Advances the stream position by the given offset. + * + * @param stream $handle Bytes will be skipped from this resource. + * @param int $num_bytes Number of skipped bytes. Can be 0. + * @return bool True on success or false on failure. + */ +// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero. +function skip( $handle, $num_bytes ) { + return ( fseek( $handle, $num_bytes, SEEK_CUR ) == 0 ); +} + +//------------------------------------------------------------------------------ +// Features are parsed into temporary property associations. + +class Tile { // Tile item id <-> parent item id associations. + public $tile_item_id; + public $parent_item_id; +} + +class Prop { // Property index <-> item id associations. + public $property_index; + public $item_id; +} + +class Dim_Prop { // Property <-> features associations. + public $property_index; + public $width; + public $height; +} + +class Chan_Prop { // Property <-> features associations. + public $property_index; + public $bit_depth; + public $num_channels; +} + +class Features { + public $has_primary_item = false; // True if "pitm" was parsed. + public $has_alpha = false; // True if an alpha "auxC" was parsed. + public $primary_item_id; + public $primary_item_features = array( // Deduced from the data below. + 'width' => UNDEFINED, // In number of pixels. + 'height' => UNDEFINED, // Ignores mirror and rotation. + 'bit_depth' => UNDEFINED, // Likely 8, 10 or 12 bits per channel per pixel. + 'num_channels' => UNDEFINED // Likely 1, 2, 3 or 4 channels: + // (1 monochrome or 3 colors) + (0 or 1 alpha) + ); + + public $tiles = array(); // Tile[] + public $props = array(); // Prop[] + public $dim_props = array(); // Dim_Prop[] + public $chan_props = array(); // Chan_Prop[] + + /** + * Binds the width, height, bit depth and number of channels from stored internal features. + * + * @param int $target_item_id Id of the item whose features will be bound. + * @param int $tile_depth Maximum recursion to search within tile-parent relations. + * @return Status FOUND on success or NOT_FOUND on failure. + */ + private function get_item_features( $target_item_id, $tile_depth ) { + foreach ( $this->props as $prop ) { + if ( $prop->item_id != $target_item_id ) { + continue; + } + + // Retrieve the width and height of the primary item if not already done. + if ( $target_item_id == $this->primary_item_id && + ( $this->primary_item_features['width'] == UNDEFINED || + $this->primary_item_features['height'] == UNDEFINED ) ) { + foreach ( $this->dim_props as $dim_prop ) { + if ( $dim_prop->property_index != $prop->property_index ) { + continue; + } + $this->primary_item_features['width'] = $dim_prop->width; + $this->primary_item_features['height'] = $dim_prop->height; + if ( $this->primary_item_features['bit_depth'] != UNDEFINED && + $this->primary_item_features['num_channels'] != UNDEFINED ) { + return FOUND; + } + break; + } + } + // Retrieve the bit depth and number of channels of the target item if not + // already done. + if ( $this->primary_item_features['bit_depth'] == UNDEFINED || + $this->primary_item_features['num_channels'] == UNDEFINED ) { + foreach ( $this->chan_props as $chan_prop ) { + if ( $chan_prop->property_index != $prop->property_index ) { + continue; + } + $this->primary_item_features['bit_depth'] = $chan_prop->bit_depth; + $this->primary_item_features['num_channels'] = $chan_prop->num_channels; + if ( $this->primary_item_features['width'] != UNDEFINED && + $this->primary_item_features['height'] != UNDEFINED ) { + return FOUND; + } + break; + } + } + } + + // Check for the bit_depth and num_channels in a tile if not yet found. + if ( $tile_depth < 3 ) { + foreach ( $this->tiles as $tile ) { + if ( $tile->parent_item_id != $target_item_id ) { + continue; + } + $status = get_item_features( $tile->tile_item_id, $tile_depth + 1 ); + if ( $status != NOT_FOUND ) { + return $status; + } + } + } + return NOT_FOUND; + } + + /** + * Finds the width, height, bit depth and number of channels of the primary item. + * + * @return Status FOUND on success or NOT_FOUND on failure. + */ + public function get_primary_item_features() { + // Nothing to do without the primary item ID. + if ( !$this->has_primary_item ) { + return NOT_FOUND; + } + // Early exit. + if ( empty( $this->dim_props ) || empty( $this->chan_props ) ) { + return NOT_FOUND; + } + $status = $this->get_item_features( $this->primary_item_id, /*tile_depth=*/ 0 ); + if ( $status != FOUND ) { + return $status; + } + + // "auxC" is parsed before the "ipma" properties so it is known now, if any. + if ( $this->has_alpha ) { + ++$this->primary_item_features['num_channels']; + } + return FOUND; + } +} + +//------------------------------------------------------------------------------ + +class Box { + public $size; // In bytes. + public $type; // Four characters. + public $version; // 0 or actual version if this is a full box. + public $flags; // 0 or actual value if this is a full box. + public $content_size; // 'size' minus the header size. + + /** + * Reads the box header. + * + * @param stream $handle The resource the header will be parsed from. + * @param int $num_parsed_boxes The total number of parsed boxes. Prevents timeouts. + * @param int $num_remaining_bytes The number of bytes that should be available from the resource. + * @return Status FOUND on success or an error on failure. + */ + public function parse( $handle, &$num_parsed_boxes, $num_remaining_bytes = MAX_SIZE ) { + // See ISO/IEC 14496-12:2012(E) 4.2 + $header_size = 8; // box 32b size + 32b type (at least) + if ( $header_size > $num_remaining_bytes ) { + return INVALID; + } + if ( !( $data = read( $handle, 8 ) ) ) { + return TRUNCATED; + } + $this->size = read_big_endian( $data, 4 ); + $this->type = substr( $data, 4, 4 ); + // 'box->size==1' means 64-bit size should be read after the box type. + // 'box->size==0' means this box extends to all remaining bytes. + if ( $this->size == 1 ) { + $header_size += 8; + if ( $header_size > $num_remaining_bytes ) { + return INVALID; + } + if ( !( $data = read( $handle, 8 ) ) ) { + return TRUNCATED; + } + // Stop the parsing if any box has a size greater than 4GB. + if ( read_big_endian( $data, 4 ) != 0 ) { + return ABORTED; + } + // Read the 32 least-significant bits. + $this->size = read_big_endian( substr( $data, 4, 4 ), 4 ); + } else if ( $this->size == 0 ) { + $this->size = $num_remaining_bytes; + } + if ( $this->size < $header_size ) { + return INVALID; + } + if ( $this->size > $num_remaining_bytes ) { + return INVALID; + } + + $has_fullbox_header = $this->type == 'meta' || $this->type == 'pitm' || + $this->type == 'ipma' || $this->type == 'ispe' || + $this->type == 'pixi' || $this->type == 'iref' || + $this->type == 'auxC'; + if ( $has_fullbox_header ) { + $header_size += 4; + } + if ( $this->size < $header_size ) { + return INVALID; + } + $this->content_size = $this->size - $header_size; + // Avoid timeouts. The maximum number of parsed boxes is arbitrary. + ++$num_parsed_boxes; + if ( $num_parsed_boxes >= MAX_NUM_BOXES ) { + return ABORTED; + } + + $this->version = 0; + $this->flags = 0; + if ( $has_fullbox_header ) { + if ( !( $data = read( $handle, 4 ) ) ) { + return TRUNCATED; + } + $this->version = read_big_endian( $data, 1 ); + $this->flags = read_big_endian( substr( $data, 1, 3 ), 3 ); + // See AV1 Image File Format (AVIF) 8.1 + // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when + // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged). + $is_parsable = ( $this->type == 'meta' && $this->version <= 0 ) || + ( $this->type == 'pitm' && $this->version <= 1 ) || + ( $this->type == 'ipma' && $this->version <= 1 ) || + ( $this->type == 'ispe' && $this->version <= 0 ) || + ( $this->type == 'pixi' && $this->version <= 0 ) || + ( $this->type == 'iref' && $this->version <= 1 ) || + ( $this->type == 'auxC' && $this->version <= 0 ); + // Instead of considering this file as invalid, skip unparsable boxes. + if ( !$is_parsable ) { + $this->type = 'unknownversion'; + } + } + // print_r( $this ); // Uncomment to print all boxes. + return FOUND; + } +} + +//------------------------------------------------------------------------------ + +class Parser { + private $handle; // Input stream. + private $num_parsed_boxes = 0; + private $data_was_skipped = false; + public $features; + + function __construct( $handle ) { + $this->handle = $handle; + $this->features = new Features(); + } + + /** + * Parses an "ipco" box. + * + * "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth + * and number of channels, and "auxC" is used for alpha. + * + * @param stream $handle The resource the box will be parsed from. + * @param int $num_remaining_bytes The number of bytes that should be available from the resource. + * @return Status FOUND on success or an error on failure. + */ + private function parse_ipco( $num_remaining_bytes ) { + $box_index = 1; // 1-based index. Used for iterating over properties. + do { + $box = new Box(); + $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes ); + if ( $status != FOUND ) { + return $status; + } + + if ( $box->type == 'ispe' ) { + // See ISO/IEC 23008-12:2017(E) 6.5.3.2 + if ( $box->content_size < 8 ) { + return INVALID; + } + if ( !( $data = read( $this->handle, 8 ) ) ) { + return TRUNCATED; + } + $width = read_big_endian( substr( $data, 0, 4 ), 4 ); + $height = read_big_endian( substr( $data, 4, 4 ), 4 ); + if ( $width == 0 || $height == 0 ) { + return INVALID; + } + if ( count( $this->features->dim_props ) <= MAX_FEATURES && + $box_index <= MAX_VALUE ) { + $dim_prop_count = count( $this->features->dim_props ); + $this->features->dim_props[$dim_prop_count] = new Dim_Prop(); + $this->features->dim_props[$dim_prop_count]->property_index = $box_index; + $this->features->dim_props[$dim_prop_count]->width = $width; + $this->features->dim_props[$dim_prop_count]->height = $height; + } else { + $this->data_was_skipped = true; + } + if ( !skip( $this->handle, $box->content_size - 8 ) ) { + return TRUNCATED; + } + } else if ( $box->type == 'pixi' ) { + // See ISO/IEC 23008-12:2017(E) 6.5.6.2 + if ( $box->content_size < 1 ) { + return INVALID; + } + if ( !( $data = read( $this->handle, 1 ) ) ) { + return TRUNCATED; + } + $num_channels = read_big_endian( $data, 1 ); + if ( $num_channels < 1 ) { + return INVALID; + } + if ( $box->content_size < 1 + $num_channels ) { + return INVALID; + } + if ( !( $data = read( $this->handle, 1 ) ) ) { + return TRUNCATED; + } + $bit_depth = read_big_endian( $data, 1 ); + if ( $bit_depth < 1 ) { + return INVALID; + } + for ( $i = 1; $i < $num_channels; ++$i ) { + if ( !( $data = read( $this->handle, 1 ) ) ) { + return TRUNCATED; + } + // Bit depth should be the same for all channels. + if ( read_big_endian( $data, 1 ) != $bit_depth ) { + return INVALID; + } + if ( $i > 32 ) { + return ABORTED; // Be reasonable. + } + } + if ( count( $this->features->chan_props ) <= MAX_FEATURES && + $box_index <= MAX_VALUE && $bit_depth <= MAX_VALUE && + $num_channels <= MAX_VALUE ) { + $chan_prop_count = count( $this->features->chan_props ); + $this->features->chan_props[$chan_prop_count] = new Chan_Prop(); + $this->features->chan_props[$chan_prop_count]->property_index = $box_index; + $this->features->chan_props[$chan_prop_count]->bit_depth = $bit_depth; + $this->features->chan_props[$chan_prop_count]->num_channels = $num_channels; + } else { + $this->data_was_skipped = true; + } + if ( !skip( $this->handle, $box->content_size - ( 1 + $num_channels ) ) ) { + return TRUNCATED; + } + } else if ( $box->type == 'av1C' ) { + // See AV1 Codec ISO Media File Format Binding 2.3.1 + // at https://aomediacodec.github.io/av1-isobmff/#av1c + // Only parse the necessary third byte. Assume that the others are valid. + if ( $box->content_size < 3 ) { + return INVALID; + } + if ( !( $data = read( $this->handle, 3 ) ) ) { + return TRUNCATED; + } + $byte = read_big_endian( substr( $data, 2, 1 ), 1 ); + $high_bitdepth = ( $byte & 0x40 ) != 0; + $twelve_bit = ( $byte & 0x20 ) != 0; + $monochrome = ( $byte & 0x10 ) != 0; + if ( $twelve_bit && !$high_bitdepth ) { + return INVALID; + } + if ( count( $this->features->chan_props ) <= MAX_FEATURES && + $box_index <= MAX_VALUE ) { + $chan_prop_count = count( $this->features->chan_props ); + $this->features->chan_props[$chan_prop_count] = new Chan_Prop(); + $this->features->chan_props[$chan_prop_count]->property_index = $box_index; + $this->features->chan_props[$chan_prop_count]->bit_depth = + $high_bitdepth ? $twelve_bit ? 12 : 10 : 8; + $this->features->chan_props[$chan_prop_count]->num_channels = $monochrome ? 1 : 3; + } else { + $this->data_was_skipped = true; + } + if ( !skip( $this->handle, $box->content_size - 3 ) ) { + return TRUNCATED; + } + } else if ( $box->type == 'auxC' ) { + // See AV1 Image File Format (AVIF) 4 + // at https://aomediacodec.github.io/av1-avif/#auxiliary-images + $kAlphaStr = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0"; + $kAlphaStrLength = 44; // Includes terminating character. + if ( $box->content_size >= $kAlphaStrLength ) { + if ( !( $data = read( $this->handle, $kAlphaStrLength ) ) ) { + return TRUNCATED; + } + if ( substr( $data, 0, $kAlphaStrLength ) == $kAlphaStr ) { + // Note: It is unlikely but it is possible that this alpha plane does + // not belong to the primary item or a tile. Ignore this issue. + $this->features->has_alpha = true; + } + if ( !skip( $this->handle, $box->content_size - $kAlphaStrLength ) ) { + return TRUNCATED; + } + } else { + if ( !skip( $this->handle, $box->content_size ) ) { + return TRUNCATED; + } + } + } else { + if ( !skip( $this->handle, $box->content_size ) ) { + return TRUNCATED; + } + } + ++$box_index; + $num_remaining_bytes -= $box->size; + } while ( $num_remaining_bytes > 0 ); + return NOT_FOUND; + } + + /** + * Parses an "iprp" box. + * + * The "ipco" box contain the properties which are linked to items by the "ipma" box. + * + * @param stream $handle The resource the box will be parsed from. + * @param int $num_remaining_bytes The number of bytes that should be available from the resource. + * @return Status FOUND on success or an error on failure. + */ + private function parse_iprp( $num_remaining_bytes ) { + do { + $box = new Box(); + $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes ); + if ( $status != FOUND ) { + return $status; + } + + if ( $box->type == 'ipco' ) { + $status = $this->parse_ipco( $box->content_size ); + if ( $status != NOT_FOUND ) { + return $status; + } + } else if ( $box->type == 'ipma' ) { + // See ISO/IEC 23008-12:2017(E) 9.3.2 + $num_read_bytes = 4; + if ( $box->content_size < $num_read_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) { + return TRUNCATED; + } + $entry_count = read_big_endian( $data, 4 ); + $id_num_bytes = ( $box->version < 1 ) ? 2 : 4; + $index_num_bytes = ( $box->flags & 1 ) ? 2 : 1; + $essential_bit_mask = ( $box->flags & 1 ) ? 0x8000 : 0x80; + + for ( $entry = 0; $entry < $entry_count; ++$entry ) { + if ( $entry >= MAX_PROPS || + count( $this->features->props ) >= MAX_PROPS ) { + $this->data_was_skipped = true; + break; + } + $num_read_bytes += $id_num_bytes + 1; + if ( $box->content_size < $num_read_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $id_num_bytes + 1 ) ) ) { + return TRUNCATED; + } + $item_id = read_big_endian( + substr( $data, 0, $id_num_bytes ), $id_num_bytes ); + $association_count = read_big_endian( + substr( $data, $id_num_bytes, 1 ), 1 ); + + for ( $property = 0; $property < $association_count; ++$property ) { + if ( $property >= MAX_PROPS || + count( $this->features->props ) >= MAX_PROPS ) { + $this->data_was_skipped = true; + break; + } + $num_read_bytes += $index_num_bytes; + if ( $box->content_size < $num_read_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $index_num_bytes ) ) ) { + return TRUNCATED; + } + $value = read_big_endian( $data, $index_num_bytes ); + // $essential = ($value & $essential_bit_mask); // Unused. + $property_index = ( $value & ~$essential_bit_mask ); + if ( $property_index <= MAX_VALUE && $item_id <= MAX_VALUE ) { + $prop_count = count( $this->features->props ); + $this->features->props[$prop_count] = new Prop(); + $this->features->props[$prop_count]->property_index = $property_index; + $this->features->props[$prop_count]->item_id = $item_id; + } else { + $this->data_was_skipped = true; + } + } + if ( $property < $association_count ) { + break; // Do not read garbage. + } + } + + // If all features are available now, do not look further. + $status = $this->features->get_primary_item_features(); + if ( $status != NOT_FOUND ) { + return $status; + } + + // Mostly if 'data_was_skipped'. + if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) { + return TRUNCATED; + } + } else { + if ( !skip( $this->handle, $box->content_size ) ) { + return TRUNCATED; + } + } + $num_remaining_bytes -= $box->size; + } while ( $num_remaining_bytes > 0 ); + return NOT_FOUND; + } + + /** + * Parses an "iref" box. + * + * The "dimg" boxes contain links between tiles and their parent items, which + * can be used to infer bit depth and number of channels for the primary item + * when the latter does not have these properties. + * + * @param stream $handle The resource the box will be parsed from. + * @param int $num_remaining_bytes The number of bytes that should be available from the resource. + * @return Status FOUND on success or an error on failure. + */ + private function parse_iref( $num_remaining_bytes ) { + do { + $box = new Box(); + $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes ); + if ( $status != FOUND ) { + return $status; + } + + if ( $box->type == 'dimg' ) { + // See ISO/IEC 14496-12:2015(E) 8.11.12.2 + $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4; + $num_read_bytes = $num_bytes_per_id + 2; + if ( $box->content_size < $num_read_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) { + return TRUNCATED; + } + $from_item_id = read_big_endian( $data, $num_bytes_per_id ); + $reference_count = read_big_endian( substr( $data, $num_bytes_per_id, 2 ), 2 ); + + for ( $i = 0; $i < $reference_count; ++$i ) { + if ( $i >= MAX_TILES ) { + $this->data_was_skipped = true; + break; + } + $num_read_bytes += $num_bytes_per_id; + if ( $box->content_size < $num_read_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) { + return TRUNCATED; + } + $to_item_id = read_big_endian( $data, $num_bytes_per_id ); + $tile_count = count( $this->features->tiles ); + if ( $from_item_id <= MAX_VALUE && $to_item_id <= MAX_VALUE && + $tile_count < MAX_TILES ) { + $this->features->tiles[$tile_count] = new Tile(); + $this->features->tiles[$tile_count]->tile_item_id = $to_item_id; + $this->features->tiles[$tile_count]->parent_item_id = $from_item_id; + } else { + $this->data_was_skipped = true; + } + } + + // If all features are available now, do not look further. + $status = $this->features->get_primary_item_features(); + if ( $status != NOT_FOUND ) { + return $status; + } + + // Mostly if 'data_was_skipped'. + if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) { + return TRUNCATED; + } + } else { + if ( !skip( $this->handle, $box->content_size ) ) { + return TRUNCATED; + } + } + $num_remaining_bytes -= $box->size; + } while ( $num_remaining_bytes > 0 ); + return NOT_FOUND; + } + + /** + * Parses a "meta" box. + * + * It looks for the primary item ID in the "pitm" box and recurses into other boxes + * to find its features. + * + * @param stream $handle The resource the box will be parsed from. + * @param int $num_remaining_bytes The number of bytes that should be available from the resource. + * @return Status FOUND on success or an error on failure. + */ + private function parse_meta( $num_remaining_bytes ) { + do { + $box = new Box(); + $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes ); + if ( $status != FOUND ) { + return $status; + } + + if ( $box->type == 'pitm' ) { + // See ISO/IEC 14496-12:2015(E) 8.11.4.2 + $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4; + if ( $num_bytes_per_id > $num_remaining_bytes ) { + return INVALID; + } + if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) { + return TRUNCATED; + } + $primary_item_id = read_big_endian( $data, $num_bytes_per_id ); + if ( $primary_item_id > MAX_VALUE ) { + return ABORTED; + } + $this->features->has_primary_item = true; + $this->features->primary_item_id = $primary_item_id; + if ( !skip( $this->handle, $box->content_size - $num_bytes_per_id ) ) { + return TRUNCATED; + } + } else if ( $box->type == 'iprp' ) { + $status = $this->parse_iprp( $box->content_size ); + if ( $status != NOT_FOUND ) { + return $status; + } + } else if ( $box->type == 'iref' ) { + $status = $this->parse_iref( $box->content_size ); + if ( $status != NOT_FOUND ) { + return $status; + } + } else { + if ( !skip( $this->handle, $box->content_size ) ) { + return TRUNCATED; + } + } + $num_remaining_bytes -= $box->size; + } while ( $num_remaining_bytes != 0 ); + // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta". + return INVALID; + } + + /** + * Parses a file stream. + * + * The file type is checked through the "ftyp" box. + * + * @return bool True if the input stream is an AVIF bitstream or false. + */ + public function parse_ftyp() { + $box = new Box(); + $status = $box->parse( $this->handle, $this->num_parsed_boxes ); + if ( $status != FOUND ) { + return false; + } + + if ( $box->type != 'ftyp' ) { + return false; + } + // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1 + if ( $box->content_size < 8 ) { + return false; + } + for ( $i = 0; $i + 4 <= $box->content_size; $i += 4 ) { + if ( !( $data = read( $this->handle, 4 ) ) ) { + return false; + } + if ( $i == 4 ) { + continue; // Skip minor_version. + } + if ( substr( $data, 0, 4 ) == 'avif' || substr( $data, 0, 4 ) == 'avis' ) { + return skip( $this->handle, $box->content_size - ( $i + 4 ) ); + } + if ( $i > 32 * 4 ) { + return false; // Be reasonable. + } + + } + return false; // No AVIF brand no good. + } + + /** + * Parses a file stream. + * + * Features are extracted from the "meta" box. + * + * @return bool True if the main features of the primary item were parsed or false. + */ + public function parse_file() { + $box = new Box(); + while ( $box->parse( $this->handle, $this->num_parsed_boxes ) == FOUND ) { + if ( $box->type === 'meta' ) { + if ( $this->parse_meta( $box->content_size ) != FOUND ) { + return false; + } + return true; + } + if ( !skip( $this->handle, $box->content_size ) ) { + return false; + } + } + return false; // No "meta" no good. + } +} diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php index de079357fb860..23331c7f196cc 100644 --- a/src/wp-includes/class-wp-image-editor-gd.php +++ b/src/wp-includes/class-wp-image-editor-gd.php @@ -71,6 +71,8 @@ public static function supports_mime_type( $mime_type ) { return ( $image_types & IMG_GIF ) != 0; case 'image/webp': return ( $image_types & IMG_WEBP ) != 0; + case 'image/avif': + return ( $image_types & IMG_AVIF ) != 0; } return false; @@ -111,6 +113,16 @@ function_exists( 'imagecreatefromwebp' ) && $this->image = @imagecreatefromstring( $file_contents ); } + // AVIF may not work with imagecreatefromstring(). + if ( + function_exists( 'imagecreatefromavif' ) && + ( 'image/avif' === wp_get_image_mime( $this->file ) ) + ) { + $this->image = @imagecreatefromavif( $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 ); } @@ -513,6 +525,10 @@ protected function _save( $image, $filename = null, $mime_type = null ) { 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' ) ); } + } elseif ( 'image/avif' == $mime_type ) { + if ( ! function_exists( 'imageavif' ) || ! $this->make_image( $filename, 'imageavif', 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' ) ); } @@ -561,8 +577,17 @@ public function stream( $mime_type = null ) { if ( function_exists( 'imagewebp' ) ) { header( 'Content-Type: image/webp' ); return imagewebp( $this->image, null, $this->get_quality() ); + } else { + // Fall back to JPEG. + header( 'Content-Type: image/jpeg' ); + return imagejpeg( $this->image, null, $this->get_quality() ); + } + case 'image/avif': + if ( function_exists( 'imageavif' ) ) { + header( 'Content-Type: image/avif' ); + return imageavif( $this->image, null, $this->get_quality() ); } - // Fall back to the default if webp isn't supported. + // Fall back to JPEG. 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 03fe0bca6975f..546ad3e7991bf 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -219,6 +219,7 @@ public function set_quality( $quality = null ) { $this->image->setImageCompressionQuality( $quality ); } break; + case 'image/avif': default: $this->image->setImageCompressionQuality( $quality ); } @@ -256,6 +257,16 @@ protected function update_size( $width = null, $height = null ) { $height = $size['height']; } + /* + * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images + * are properly sized without affecting previous `getImageGeometry` behavior. + */ + if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) { + $size = wp_getimagesize( $this->file ); + $width = $size[0]; + $height = $size[1]; + } + return parent::update_size( $width, $height ); } diff --git a/src/wp-includes/class-wp-image-editor.php b/src/wp-includes/class-wp-image-editor.php index 3c636dc6ba5a7..6604685a024f0 100644 --- a/src/wp-includes/class-wp-image-editor.php +++ b/src/wp-includes/class-wp-image-editor.php @@ -318,6 +318,7 @@ protected function get_default_quality( $mime_type ) { $quality = 86; break; case 'image/jpeg': + case 'image/avif': default: $quality = $this->default_quality; } diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php index 09905bee1b93f..2058a9e557c46 100644 --- a/src/wp-includes/class-wp-theme.php +++ b/src/wp-includes/class-wp-theme.php @@ -1263,7 +1263,7 @@ public function get_screenshot( $uri = 'uri' ) { return false; } - foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp' ) as $ext ) { + foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp', 'avif' ) 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 429c5f92e7e5c..95c4af484d8ea 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -529,3 +529,13 @@ function str_ends_with( $haystack, $needle ) { if ( ! defined( 'IMG_WEBP' ) ) { define( 'IMG_WEBP', IMAGETYPE_WEBP ); } + +// IMAGETYPE_AVIF constant is only defined in PHP 8.x or later. +if ( ! defined( 'IMAGETYPE_AVIF' ) ) { + define( 'IMAGETYPE_AVIF', 19 ); +} + +// IMG_AVIF constant is only defined in PHP 8.x or later. +if ( ! defined( 'IMG_AVIF' ) ) { + define( 'IMG_AVIF', IMAGETYPE_AVIF ); +} 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 f63689292522b..3bdc4e2dc1a7c 100644 --- a/src/wp-includes/customize/class-wp-customize-media-control.php +++ b/src/wp-includes/customize/class-wp-customize-media-control.php @@ -93,7 +93,7 @@ public function to_json() { * 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', 'webp' ), true ) ? 'image' : 'document'; + $type = in_array( $ext, array( 'jpg', 'png', 'gif', 'bmp', 'webp', 'avif' ), true ) ? 'image' : 'document'; $default_attachment = array( 'id' => 1, diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 1fd62ae16e898..45b4f89a9e4be 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -3336,7 +3336,9 @@ function gd_edit_image_support($mime_type) { return (imagetypes() & IMG_GIF) != 0; case 'image/webp': return (imagetypes() & IMG_WEBP) != 0; - } + case 'image/avif': + return (imagetypes() & IMG_AVIF) != 0; + } } else { switch( $mime_type ) { case 'image/jpeg': @@ -3347,6 +3349,8 @@ function gd_edit_image_support($mime_type) { return function_exists('imagecreatefromgif'); case 'image/webp': return function_exists('imagecreatefromwebp'); + case 'image/avif': + return function_exists('imagecreatefromavif'); } } return false; diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 05b103e6f755f..ce8d0354209e2 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -3464,7 +3464,7 @@ function translate_smiley( $matches ) { $matches = array(); $ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false; - $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); + $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' ); // 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 94995206f146a..17319c7f4683d 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3117,6 +3117,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { 'image/bmp' => 'bmp', 'image/tiff' => 'tif', 'image/webp' => 'webp', + 'image/avif' => 'avif', ) ); @@ -3295,6 +3296,7 @@ function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { * * @since 4.7.1 * @since 5.8.0 Added support for WebP images. + * @since 6.5.0 Added support for AVIF images. * * @param string $file Full path to the file. * @return string|false The actual mime type or false if the type cannot be determined. @@ -3349,6 +3351,25 @@ function wp_get_image_mime( $file ) { ) { $mime = 'image/webp'; } + + /** + * Add AVIF fallback detection when image library doesn't support AVIF. + * + * Detection based on section 4.3.1 File-type box definition of the ISO/IEC 14496-12 + * specification and the AV1-AVIF spec, see https://aomediacodec.github.io/av1-avif/v1.1.0.html#brands. + */ + + // Divide the header string into 4 byte groups. + $magic = str_split( $magic, 8 ); + + if ( + isset( $magic[1] ) && + isset( $magic[2] ) && + 'ftyp' === hex2bin( $magic[1] ) && + ( 'avif' === hex2bin( $magic[2] ) || 'avis' === hex2bin( $magic[2] ) ) + ) { + $mime = 'image/avif'; + } } catch ( Exception $e ) { $mime = false; } @@ -3388,6 +3409,7 @@ function wp_get_mime_types() { 'bmp' => 'image/bmp', 'tiff|tif' => 'image/tiff', 'webp' => 'image/webp', + 'avif' => 'image/avif', 'ico' => 'image/x-icon', 'heic' => 'image/heic', // Video formats. @@ -3509,7 +3531,7 @@ function wp_get_ext_types() { return apply_filters( 'ext2type', array( - 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp' ), + 'image' => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp', 'avif' ), '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 38ec2213b7506..69a6d53299cab 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4100,6 +4100,7 @@ function _wp_image_editor_choose( $args = array() ) { require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; + require_once ABSPATH . WPINC . '/class-avif-info.php'; /** * Filters the list of image editing library classes. * @@ -4204,6 +4205,11 @@ function wp_plupload_default_settings() { $defaults['webp_upload_error'] = true; } + // Check if AVIF images can be edited. + if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) { + $defaults['avif_upload_error'] = true; + } + /** * Filters the Plupload default settings. * @@ -5480,6 +5486,7 @@ function wp_show_heic_upload_error( $plupload_settings ) { * * @since 5.7.0 * @since 5.8.0 Added support for WebP images. + * @since 6.5.0 Added support for AVIF images. * * @param string $filename The file path. * @param array $image_info Optional. Extended image information (passed by reference). @@ -5512,7 +5519,11 @@ function wp_getimagesize( $filename, array &$image_info = null ) { } } - if ( false !== $info ) { + if ( + ! empty( $info ) && + // Some PHP versions return 0x0 sizes from `getimagesize` for unrecognized image formats, including AVIFs. + ! ( empty( $info[0] ) && empty( $info[1] ) ) + ) { return $info; } @@ -5541,10 +5552,75 @@ function wp_getimagesize( $filename, array &$image_info = null ) { } } + // For PHP versions that don't support AVIF images, extract the image size info from the file headers. + if ( 'image/avif' === wp_get_image_mime( $filename ) ) { + $avif_info = wp_get_avif_info( $filename ); + + $width = $avif_info['width']; + $height = $avif_info['height']; + + // Mimic the native return format. + if ( $width && $height ) { + return array( + $width, + $height, + IMAGETYPE_AVIF, + sprintf( + 'width="%d" height="%d"', + $width, + $height + ), + 'mime' => 'image/avif', + ); + } + } + // The image could not be parsed. return false; } +/** + * Extracts meta information about an AVIF file: width, height, bit depth, and number of channels. + * + * @since 6.5.0 + * + * @param string $filename Path to an AVIF file. + * @return array { + * An array of AVIF image information. + * + * @type int|false $width Image width on success, false on failure. + * @type int|false $height Image height on success, false on failure. + * @type int|false $bit_depth Image bit depth on success, false on failure. + * @type int|false $num_channels Image number of channels on success, false on failure. + * } + */ +function wp_get_avif_info( $filename ) { + $results = array( + 'width' => false, + 'height' => false, + 'bit_depth' => false, + 'num_channels' => false, + ); + + if ( 'image/avif' !== wp_get_image_mime( $filename ) ) { + return $results; + } + + // Parse the file using libavifinfo's PHP implementation. + require_once ABSPATH . WPINC . '/class-avif-info.php'; + + $handle = fopen( $filename, 'rb' ); + if ( $handle ) { + $parser = new Avifinfo\Parser( $handle ); + $success = $parser->parse_ftyp() && $parser->parse_file(); + fclose( $handle ); + if ( $success ) { + $results = $parser->features->primary_item_features; + } + } + return $results; +} + /** * Extracts meta information about a WebP file: width, height, and type. * diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index 69a63625524d1..001ccaf7d0465 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -6700,7 +6700,7 @@ function wp_attachment_is( $type, $post = null ) { switch ( $type ) { case 'image': - $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' ); + $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' ); 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 d6bba7a9249f1..8a26a79a67932 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 @@ -456,7 +456,7 @@ public function edit_media_item( $request ) { ); } - $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' ); + $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' ); $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/avif-animated.avif b/tests/phpunit/data/images/avif-animated.avif new file mode 100644 index 0000000000000..6d6a34a7305f8 Binary files /dev/null and b/tests/phpunit/data/images/avif-animated.avif differ diff --git a/tests/phpunit/data/images/avif-lossless.avif b/tests/phpunit/data/images/avif-lossless.avif new file mode 100644 index 0000000000000..7eb2d5ce68ee6 Binary files /dev/null and b/tests/phpunit/data/images/avif-lossless.avif differ diff --git a/tests/phpunit/data/images/avif-lossy.avif b/tests/phpunit/data/images/avif-lossy.avif new file mode 100644 index 0000000000000..0aba41c1bf2e9 Binary files /dev/null and b/tests/phpunit/data/images/avif-lossy.avif differ diff --git a/tests/phpunit/data/images/avif-transparent.avif b/tests/phpunit/data/images/avif-transparent.avif new file mode 100644 index 0000000000000..8165f9ff46a24 Binary files /dev/null and b/tests/phpunit/data/images/avif-transparent.avif differ diff --git a/tests/phpunit/data/images/color_grid_alpha_nogrid.avif b/tests/phpunit/data/images/color_grid_alpha_nogrid.avif new file mode 100644 index 0000000000000..fa301f589822c Binary files /dev/null and b/tests/phpunit/data/images/color_grid_alpha_nogrid.avif differ diff --git a/tests/phpunit/data/images/colors_hdr_p3.avif b/tests/phpunit/data/images/colors_hdr_p3.avif new file mode 100644 index 0000000000000..6a2403f110da5 Binary files /dev/null and b/tests/phpunit/data/images/colors_hdr_p3.avif differ diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index 3c2e1101ad69d..abef68f75b085 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1370,6 +1370,26 @@ public function data_wp_get_image_mime() { DIR_TESTDATA . '/uploads/dashicons.woff', false, ), + // Animated AVIF. + array( + DIR_TESTDATA . '/images/avif-animated.avif', + 'image/avif', + ), + // Lossless AVIF. + array( + DIR_TESTDATA . '/images/avif-lossless.avif', + 'image/avif', + ), + // Lossy AVIF. + array( + DIR_TESTDATA . '/images/avif-lossy.avif', + 'image/avif', + ), + // Transparent AVIF. + array( + DIR_TESTDATA . '/images/avif-transparent.avif', + 'image/avif', + ), ); return $data; @@ -1496,6 +1516,50 @@ public function data_wp_getimagesize() { DIR_TESTDATA . '/uploads/dashicons.woff', false, ), + // Animated AVIF. + array( + DIR_TESTDATA . '/images/avif-animated.avif', + array( + 150, + 150, + IMAGETYPE_AVIF, + 'width="150" height="150"', + 'mime' => 'image/avif', + ), + ), + // Lossless AVIF. + array( + DIR_TESTDATA . '/images/avif-lossless.avif', + array( + 400, + 400, + IMAGETYPE_AVIF, + 'width="400" height="400"', + 'mime' => 'image/avif', + ), + ), + // Lossy AVIF. + array( + DIR_TESTDATA . '/images/avif-lossy.avif', + array( + 400, + 400, + IMAGETYPE_AVIF, + 'width="400" height="400"', + 'mime' => 'image/avif', + ), + ), + // Transparent AVIF. + array( + DIR_TESTDATA . '/images/avif-transparent.avif', + array( + 128, + 128, + IMAGETYPE_AVIF, + 'width="128" height="128"', + 'mime' => 'image/avif', + ), + ), ); return $data; diff --git a/tests/phpunit/tests/image/editor.php b/tests/phpunit/tests/image/editor.php index bd54b803e2897..5e857bf47258c 100644 --- a/tests/phpunit/tests/image/editor.php +++ b/tests/phpunit/tests/image/editor.php @@ -292,12 +292,6 @@ public function test_get_suffix() { * */ 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( 'No WebP support in the editor engine %s on this system.', $this->editor_engine ) ); - } - $file_data = wp_get_webp_info( $file ); $this->assertSame( $expected, $file_data ); } @@ -363,4 +357,105 @@ public function data_wp_get_webp_info() { ), ); } + + /** + * Test wp_get_avif_info. + * + * @ticket 51228 + * + * @dataProvider data_wp_get_avif_info + * + * @param string $file The path to the AVIF file for testing. + * @param array $expected The expected AVIF file information. + */ + public function test_wp_get_avif_info( $file, $expected ) { + $file_data = wp_get_avif_info( $file ); + $this->assertSame( $expected, $file_data ); + } + + /** + * Data provider for test_wp_get_avif_info(). + */ + public function data_wp_get_avif_info() { + return array( + // Standard JPEG. + array( + DIR_TESTDATA . '/images/test-image.jpg', + array( + 'width' => false, + 'height' => false, + 'bit_depth' => false, + 'num_channels' => false, + ), + ), + // Standard GIF. + array( + DIR_TESTDATA . '/images/test-image.gif', + array( + 'width' => false, + 'height' => false, + 'bit_depth' => false, + 'num_channels' => false, + ), + ), + // Animated AVIF. + array( + DIR_TESTDATA . '/images/avif-animated.avif', + array( + 'width' => 150, + 'height' => 150, + 'bit_depth' => 8, + 'num_channels' => 4, + ), + ), + // Lossless AVIF. + array( + DIR_TESTDATA . '/images/avif-lossless.avif', + array( + 'width' => 400, + 'height' => 400, + 'bit_depth' => 8, + 'num_channels' => 3, + ), + ), + // Lossy AVIF. + array( + DIR_TESTDATA . '/images/avif-lossy.avif', + array( + 'width' => 400, + 'height' => 400, + 'bit_depth' => 8, + 'num_channels' => 3, + ), + ), + // Transparent AVIF. + array( + DIR_TESTDATA . '/images/avif-transparent.avif', + array( + 'width' => 128, + 'height' => 128, + 'bit_depth' => 12, + 'num_channels' => 4, + ), + ), + array( + DIR_TESTDATA . '/images/color_grid_alpha_nogrid.avif', + array( + 'width' => 80, + 'height' => 80, + 'bit_depth' => 8, + 'num_channels' => 4, + ), + ), + array( + DIR_TESTDATA . '/images/colors_hdr_p3.avif', + array( + 'width' => 200, + 'height' => 200, + 'bit_depth' => 10, + 'num_channels' => 3, + ), + ), + ); + } } diff --git a/tests/phpunit/tests/image/functions.php b/tests/phpunit/tests/image/functions.php index f55b164496a84..56c81a62f0c72 100644 --- a/tests/phpunit/tests/image/functions.php +++ b/tests/phpunit/tests/image/functions.php @@ -111,6 +111,10 @@ public function data_file_is_valid_image_positive() { 'webp-lossless.webp', 'webp-lossy.webp', 'webp-transparent.webp', + 'avif-animated.avif', + 'avif-lossless.avif', + 'avif-lossy.avif', + 'avif-transparent.avif', ); return $this->text_array_to_dataprovider( $files ); @@ -186,6 +190,17 @@ public function data_file_is_displayable_image_positive() { $files[] = 'webp-transparent.webp'; } + // Add AVIF images if the image editor supports them. + $file = DIR_TESTDATA . '/images/avif-lossless.avif'; + $editor = wp_get_image_editor( $file ); + + if ( ! is_wp_error( $editor ) && $editor->supports_mime_type( 'image/avif' ) ) { + $files[] = 'avif-animated.avif'; + $files[] = 'avif-lossless.avif'; + $files[] = 'avif-lossy.avif'; + $files[] = 'avif-transparent.avif'; + } + return $this->text_array_to_dataprovider( $files ); } diff --git a/tests/phpunit/tests/image/resize.php b/tests/phpunit/tests/image/resize.php index 5b302ce2958b8..e82dd3d2e6b2a 100644 --- a/tests/phpunit/tests/image/resize.php +++ b/tests/phpunit/tests/image/resize.php @@ -88,6 +88,32 @@ public function test_resize_webp() { $this->assertSame( IMAGETYPE_WEBP, $type ); } + /** + * Test resizing AVIF image. + * + * @ticket 51228 + */ + public function test_resize_avif() { + $file = DIR_TESTDATA . '/images/avif-lossy.avif'; + $editor = wp_get_image_editor( $file ); + + // Check if the editor supports the avif mime type. + if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/avif' ) ) { + $this->markTestSkipped( sprintf( 'No AVIF support in the editor engine %s on this system.', $this->editor_engine ) ); + } + + $image = $this->resize_helper( $file, 25, 25 ); + + list( $w, $h, $type ) = wp_getimagesize( $image ); + + unlink( $image ); + + $this->assertSame( 'avif-lossy-25x25.avif', wp_basename( $image ) ); + $this->assertSame( 25, $w ); + $this->assertSame( 25, $h ); + $this->assertSame( IMAGETYPE_AVIF, $type ); + } + public 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 );