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 );