Make WordPress Core

Ticket #51228: 51228.2.diff

File 51228.2.diff, 59.4 KB (added by adamsilverstein, 13 months ago)
  • phpcs.xml.dist

    diff --git a/phpcs.xml.dist b/phpcs.xml.dist
    index ccb0430321..840bdad8fe 100644
    a b  
    5959        <exclude-pattern>/src/wp-includes/class-requests\.php</exclude-pattern>
    6060        <exclude-pattern>/src/wp-includes/class-simplepie\.php</exclude-pattern>
    6161        <exclude-pattern>/src/wp-includes/class-snoopy\.php</exclude-pattern>
     62        <exclude-pattern>/src/wp-includes/class-avif-info\.php</exclude-pattern>
    6263        <exclude-pattern>/src/wp-includes/deprecated\.php</exclude-pattern>
    6364        <exclude-pattern>/src/wp-includes/ms-deprecated\.php</exclude-pattern>
    6465        <exclude-pattern>/src/wp-includes/pluggable-deprecated\.php</exclude-pattern>
  • src/js/_enqueues/vendor/plupload/handlers.js

    diff --git a/src/js/_enqueues/vendor/plupload/handlers.js b/src/js/_enqueues/vendor/plupload/handlers.js
    index b82a6e8847..71e248fb5e 100644
    a b jQuery( document ).ready( function( $ ) { 
    608608                                        wpQueueError( pluploadL10n.noneditable_image );
    609609                                        up.removeFile( file );
    610610                                        return;
     611                                } else if ( file.type === 'image/avif' && up.settings.avif_upload_error ) {
     612                                        // Disallow uploading of AVIF images if the server cannot edit them.
     613                                        wpQueueError( pluploadL10n.noneditable_image );
     614                                        up.removeFile( file );
     615                                        return;
    611616                                }
    612617
    613618                                fileQueued( file );
  • src/js/_enqueues/vendor/plupload/wp-plupload.js

    diff --git a/src/js/_enqueues/vendor/plupload/wp-plupload.js b/src/js/_enqueues/vendor/plupload/wp-plupload.js
    index 0fdebf77d1..c0eb570657 100644
    a b window.wp = window.wp || {}; 
    363363                                        error( pluploadL10n.noneditable_image, {}, file, 'no-retry' );
    364364                                        up.removeFile( file );
    365365                                        return;
     366                                } else if ( file.type === 'image/avif' && up.settings.avif_upload_error ) {
     367                                        // Disallow uploading of AVIF images if the server cannot edit them.
     368                                        error( pluploadL10n.noneditable_image, {}, file, 'no-retry' );
     369                                        up.removeFile( file );
     370                                        return;
    366371                                }
    367372
    368373                                // Generate attributes for a new `Attachment` model.
  • src/js/_enqueues/vendor/thickbox/thickbox.js

    diff --git a/src/js/_enqueues/vendor/thickbox/thickbox.js b/src/js/_enqueues/vendor/thickbox/thickbox.js
    index 5470467a1e..e8b95677c1 100644
    a b function tb_show(caption, url, imageGroup) {//function called when the user clic 
    7676                        baseURL = url;
    7777           }
    7878
    79            var urlString = /\.jpg$|\.jpeg$|\.png$|\.gif$|\.bmp$|\.webp$/;
     79           var urlString = /\.jpg$|\.jpeg$|\.png$|\.gif$|\.bmp$|\.webp$|\.avif$/;
    8080           var urlType = baseURL.toLowerCase().match(urlString);
    8181
    8282                if(urlType == '.jpg' ||
    function tb_show(caption, url, imageGroup) {//function called when the user clic 
    8484                        urlType == '.png' ||
    8585                        urlType == '.gif' ||
    8686                        urlType == '.bmp' ||
    87                         urlType == '.webp'
     87                        urlType == '.webp' ||
     88                        urlType == '.avif'
    8889                ){//code to show images
    8990
    9091                        TB_PrevCaption = "";
  • src/js/media/controllers/library.js

    diff --git a/src/js/media/controllers/library.js b/src/js/media/controllers/library.js
    index 2acc89a586..126ce8d783 100644
    a b Library = wp.media.controller.State.extend(/** @lends wp.media.controller.Librar 
    196196        isImageAttachment: function( attachment ) {
    197197                // If uploading, we know the filename but not the mime type.
    198198                if ( attachment.get('uploading') ) {
    199                         return /\.(jpe?g|png|gif|webp)$/i.test( attachment.get('filename') );
     199                        return /\.(jpe?g|png|gif|webp|avif)$/i.test( attachment.get('filename') );
    200200                }
    201201
    202202                return attachment.get('type') === 'image';
  • src/wp-admin/includes/image-edit.php

    diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php
    index 739b09f9a1..2d150e691c 100644
    a b function wp_stream_image( $image, $mime_type, $attachment_id ) { 
    390390                                        return imagewebp( $image, null, 90 );
    391391                                }
    392392                                return false;
     393                        case 'image/avif':
     394                                if ( function_exists( 'imageavif' ) ) {
     395                                        header( 'Content-Type: image/avif' );
     396                                        return imageavif( $image, null, 90 );
     397                                }
     398                                return false;
    393399                        default:
    394400                                return false;
    395401                }
    function wp_save_image_file( $filename, $image, $mime_type, $post_id ) { 
    494500                                        return imagewebp( $image, $filename );
    495501                                }
    496502                                return false;
     503                        case 'image/avif':
     504                                if ( function_exists( 'imageavif' ) ) {
     505                                        return imageavif( $image, $filename );
     506                                }
     507                                return false;
    497508                        default:
    498509                                return false;
    499510                }
  • src/wp-admin/includes/image.php

    diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php
    index d60ec8508b..0f4ba818e6 100644
    a b function file_is_valid_image( $path ) { 
    10061006 * @return bool True if suitable, false if not suitable.
    10071007 */
    10081008function file_is_displayable_image( $path ) {
    1009         $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP );
     1009        $displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP, IMAGETYPE_AVIF );
    10101010
    10111011        $info = wp_getimagesize( $path );
    10121012        if ( empty( $info ) ) {
  • src/wp-admin/includes/media.php

    diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php
    index 57df2bcbff..193e67f7dd 100644
    a b function media_upload_form( $errors = null ) { 
    21982198                $plupload_init['webp_upload_error'] = true;
    21992199        }
    22002200
     2201        // Check if AVIF images can be edited.
     2202        if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) {
     2203                $plupload_init['avif_upload_error'] = true;
     2204        }
     2205
    22012206        /**
    22022207         * Filters the default Plupload settings.
    22032208         *
  • src/wp-admin/includes/schema.php

    diff --git a/src/wp-admin/includes/schema.php b/src/wp-admin/includes/schema.php
    index 20648d7dde..63655ccf17 100644
    a b We hope you enjoy your new site. Thanks! 
    12501250                'png',
    12511251                'gif',
    12521252                'webp',
     1253                'avif',
    12531254                // Video.
    12541255                'mov',
    12551256                'avi',
  • new file src/wp-includes/class-avif-info.php

    diff --git a/src/wp-includes/class-avif-info.php b/src/wp-includes/class-avif-info.php
    new file mode 100644
    index 0000000000..93280b3806
    - +  
     1<?php
     2/**
     3 * Copyright (c) 2021, Alliance for Open Media. All rights reserved
     4 *
     5 * This source code is subject to the terms of the BSD 2 Clause License and
     6 * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
     7 * was not distributed with this source code in the LICENSE file, you can
     8 * obtain it at www.aomedia.org/license/software. If the Alliance for Open
     9 * Media Patent License 1.0 was not distributed with this source code in the
     10 * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
     11 *
     12 * Note: this class is from libavifinfo - https://aomedia.googlesource.com/libavifinfo/+/refs/heads/main/avifinfo.php at f509487.
     13 * It is used as a fallback to parse AVIF files when the server doesn't support AVIF,
     14 * primarily to identify the width and height of the image.
     15 *
     16 * Note PHP 8.2 added native support for AVIF, so this class can be removed when WordPress requires PHP 8.2.
     17 */
     18
     19namespace Avifinfo;
     20
     21const FOUND     = 0; // Input correctly parsed and information retrieved.
     22const NOT_FOUND = 1; // Input correctly parsed but information is missing or elsewhere.
     23const TRUNCATED = 2; // Input correctly parsed until missing bytes to continue.
     24const ABORTED   = 3; // Input correctly parsed until stopped to avoid timeout or crash.
     25const INVALID   = 4; // Input incorrectly parsed.
     26
     27const MAX_SIZE      = 4294967295; // Unlikely to be insufficient to parse AVIF headers.
     28const MAX_NUM_BOXES = 4096;       // Be reasonable. Avoid timeouts and out-of-memory.
     29const MAX_VALUE     = 255;
     30const MAX_TILES     = 16;
     31const MAX_PROPS     = 32;
     32const MAX_FEATURES  = 8;
     33const UNDEFINED     = 0;          // Value was not yet parsed.
     34
     35/**
     36 * Reads an unsigned integer with most significant bits first.
     37 *
     38 * @param binary string $input     Must be at least $num_bytes-long.
     39 * @param int           $num_bytes Number of parsed bytes.
     40 * @return int                     Value.
     41 */
     42function read_big_endian( $input, $num_bytes ) {
     43  if ( $num_bytes == 1 ) {
     44    return unpack( 'C', $input ) [1];
     45  } else if ( $num_bytes == 2 ) {
     46    return unpack( 'n', $input ) [1];
     47  } else if ( $num_bytes == 3 ) {
     48    $bytes = unpack( 'C3', $input );
     49    return ( $bytes[1] << 16 ) | ( $bytes[2] << 8 ) | $bytes[3];
     50  } else { // $num_bytes is 4
     51    // This might fail to read unsigned values >= 2^31 on 32-bit systems.
     52    // See https://www.php.net/manual/en/function.unpack.php#106041
     53    return unpack( 'N', $input ) [1];
     54  }
     55}
     56
     57/**
     58 * Reads bytes and advances the stream position by the same count.
     59 *
     60 * @param stream               $handle    Bytes will be read from this resource.
     61 * @param int                  $num_bytes Number of bytes read. Must be greater than 0.
     62 * @return binary string|false            The raw bytes or false on failure.
     63 */
     64function read( $handle, $num_bytes ) {
     65  $data = fread( $handle, $num_bytes );
     66  return ( $data !== false && strlen( $data ) >= $num_bytes ) ? $data : false;
     67}
     68
     69/**
     70 * Advances the stream position by the given offset.
     71 *
     72 * @param stream $handle    Bytes will be skipped from this resource.
     73 * @param int    $num_bytes Number of skipped bytes. Can be 0.
     74 * @return bool             True on success or false on failure.
     75 */
     76// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero.
     77function skip( $handle, $num_bytes ) {
     78  return ( fseek( $handle, $num_bytes, SEEK_CUR ) == 0 );
     79}
     80
     81//------------------------------------------------------------------------------
     82// Features are parsed into temporary property associations.
     83
     84class Tile { // Tile item id <-> parent item id associations.
     85  public $tile_item_id;
     86  public $parent_item_id;
     87}
     88
     89class Prop { // Property index <-> item id associations.
     90  public $property_index;
     91  public $item_id;
     92}
     93
     94class Dim_Prop { // Property <-> features associations.
     95  public $property_index;
     96  public $width;
     97  public $height;
     98}
     99
     100class Chan_Prop { // Property <-> features associations.
     101  public $property_index;
     102  public $bit_depth;
     103  public $num_channels;
     104}
     105
     106class Features {
     107  public $has_primary_item = false; // True if "pitm" was parsed.
     108  public $has_alpha = false; // True if an alpha "auxC" was parsed.
     109  public $primary_item_id;
     110  public $primary_item_features = array( // Deduced from the data below.
     111    'width'        => UNDEFINED, // In number of pixels.
     112    'height'       => UNDEFINED, // Ignores mirror and rotation.
     113    'bit_depth'    => UNDEFINED, // Likely 8, 10 or 12 bits per channel per pixel.
     114    'num_channels' => UNDEFINED  // Likely 1, 2, 3 or 4 channels:
     115                                          //   (1 monochrome or 3 colors) + (0 or 1 alpha)
     116  );
     117
     118  public $tiles = array(); // Tile[]
     119  public $props = array(); // Prop[]
     120  public $dim_props = array(); // Dim_Prop[]
     121  public $chan_props = array(); // Chan_Prop[]
     122
     123  /**
     124   * Binds the width, height, bit depth and number of channels from stored internal features.
     125   *
     126   * @param int     $target_item_id Id of the item whose features will be bound.
     127   * @param int     $tile_depth     Maximum recursion to search within tile-parent relations.
     128   * @return Status                 FOUND on success or NOT_FOUND on failure.
     129   */
     130  private function get_item_features( $target_item_id, $tile_depth ) {
     131    foreach ( $this->props as $prop ) {
     132      if ( $prop->item_id != $target_item_id ) {
     133        continue;
     134      }
     135
     136      // Retrieve the width and height of the primary item if not already done.
     137      if ( $target_item_id == $this->primary_item_id &&
     138           ( $this->primary_item_features['width'] == UNDEFINED ||
     139             $this->primary_item_features['height'] == UNDEFINED ) ) {
     140        foreach ( $this->dim_props as $dim_prop ) {
     141          if ( $dim_prop->property_index != $prop->property_index ) {
     142            continue;
     143          }
     144          $this->primary_item_features['width']  = $dim_prop->width;
     145          $this->primary_item_features['height'] = $dim_prop->height;
     146          if ( $this->primary_item_features['bit_depth'] != UNDEFINED &&
     147               $this->primary_item_features['num_channels'] != UNDEFINED ) {
     148            return FOUND;
     149          }
     150          break;
     151        }
     152      }
     153      // Retrieve the bit depth and number of channels of the target item if not
     154      // already done.
     155      if ( $this->primary_item_features['bit_depth'] == UNDEFINED ||
     156           $this->primary_item_features['num_channels'] == UNDEFINED ) {
     157        foreach ( $this->chan_props as $chan_prop ) {
     158          if ( $chan_prop->property_index != $prop->property_index ) {
     159            continue;
     160          }
     161          $this->primary_item_features['bit_depth']    = $chan_prop->bit_depth;
     162          $this->primary_item_features['num_channels'] = $chan_prop->num_channels;
     163          if ( $this->primary_item_features['width'] != UNDEFINED &&
     164              $this->primary_item_features['height'] != UNDEFINED ) {
     165            return FOUND;
     166          }
     167          break;
     168        }
     169      }
     170    }
     171
     172    // Check for the bit_depth and num_channels in a tile if not yet found.
     173    if ( $tile_depth < 3 ) {
     174      foreach ( $this->tiles as $tile ) {
     175        if ( $tile->parent_item_id != $target_item_id ) {
     176          continue;
     177        }
     178        $status = get_item_features( $tile->tile_item_id, $tile_depth + 1 );
     179        if ( $status != NOT_FOUND ) {
     180          return $status;
     181        }
     182      }
     183    }
     184    return NOT_FOUND;
     185  }
     186
     187  /**
     188   * Finds the width, height, bit depth and number of channels of the primary item.
     189   *
     190   * @return Status FOUND on success or NOT_FOUND on failure.
     191   */
     192  public function get_primary_item_features() {
     193    // Nothing to do without the primary item ID.
     194    if ( !$this->has_primary_item ) {
     195      return NOT_FOUND;
     196    }
     197    // Early exit.
     198    if ( empty( $this->dim_props ) || empty( $this->chan_props ) ) {
     199      return NOT_FOUND;
     200    }
     201    $status = $this->get_item_features( $this->primary_item_id, /*tile_depth=*/ 0 );
     202    if ( $status != FOUND ) {
     203      return $status;
     204    }
     205
     206    // "auxC" is parsed before the "ipma" properties so it is known now, if any.
     207    if ( $this->has_alpha ) {
     208      ++$this->primary_item_features['num_channels'];
     209    }
     210    return FOUND;
     211  }
     212}
     213
     214//------------------------------------------------------------------------------
     215
     216class Box {
     217  public $size; // In bytes.
     218  public $type; // Four characters.
     219  public $version; // 0 or actual version if this is a full box.
     220  public $flags; // 0 or actual value if this is a full box.
     221  public $content_size; // 'size' minus the header size.
     222
     223  /**
     224   * Reads the box header.
     225   *
     226   * @param stream  $handle              The resource the header will be parsed from.
     227   * @param int     $num_parsed_boxes    The total number of parsed boxes. Prevents timeouts.
     228   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     229   * @return Status                      FOUND on success or an error on failure.
     230   */
     231  public function parse( $handle, &$num_parsed_boxes, $num_remaining_bytes = MAX_SIZE ) {
     232    // See ISO/IEC 14496-12:2012(E) 4.2
     233    $header_size = 8; // box 32b size + 32b type (at least)
     234    if ( $header_size > $num_remaining_bytes ) {
     235      return INVALID;
     236    }
     237    if ( !( $data = read( $handle, 8 ) ) ) {
     238      return TRUNCATED;
     239    }
     240    $this->size = read_big_endian( $data, 4 );
     241    $this->type = substr( $data, 4, 4 );
     242    // 'box->size==1' means 64-bit size should be read after the box type.
     243    // 'box->size==0' means this box extends to all remaining bytes.
     244    if ( $this->size == 1 ) {
     245      $header_size += 8;
     246      if ( $header_size > $num_remaining_bytes ) {
     247        return INVALID;
     248      }
     249      if ( !( $data = read( $handle, 8 ) ) ) {
     250        return TRUNCATED;
     251      }
     252      // Stop the parsing if any box has a size greater than 4GB.
     253      if ( read_big_endian( $data, 4 ) != 0 ) {
     254        return ABORTED;
     255      }
     256      // Read the 32 least-significant bits.
     257      $this->size = read_big_endian( substr( $data, 4, 4 ), 4 );
     258    } else if ( $this->size == 0 ) {
     259      $this->size = $num_remaining_bytes;
     260    }
     261    if ( $this->size < $header_size ) {
     262      return INVALID;
     263    }
     264    if ( $this->size > $num_remaining_bytes ) {
     265      return INVALID;
     266    }
     267
     268    $has_fullbox_header = $this->type == 'meta' || $this->type == 'pitm' ||
     269                          $this->type == 'ipma' || $this->type == 'ispe' ||
     270                          $this->type == 'pixi' || $this->type == 'iref' ||
     271                          $this->type == 'auxC';
     272    if ( $has_fullbox_header ) {
     273      $header_size += 4;
     274    }
     275    if ( $this->size < $header_size ) {
     276      return INVALID;
     277    }
     278    $this->content_size = $this->size - $header_size;
     279    // Avoid timeouts. The maximum number of parsed boxes is arbitrary.
     280    ++$num_parsed_boxes;
     281    if ( $num_parsed_boxes >= MAX_NUM_BOXES ) {
     282      return ABORTED;
     283    }
     284
     285    $this->version = 0;
     286    $this->flags   = 0;
     287    if ( $has_fullbox_header ) {
     288      if ( !( $data = read( $handle, 4 ) ) ) {
     289        return TRUNCATED;
     290      }
     291      $this->version = read_big_endian( $data, 1 );
     292      $this->flags   = read_big_endian( substr( $data, 1, 3 ), 3 );
     293      // See AV1 Image File Format (AVIF) 8.1
     294      // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when
     295      // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged).
     296      $is_parsable = ( $this->type == 'meta' && $this->version <= 0 ) ||
     297                     ( $this->type == 'pitm' && $this->version <= 1 ) ||
     298                     ( $this->type == 'ipma' && $this->version <= 1 ) ||
     299                     ( $this->type == 'ispe' && $this->version <= 0 ) ||
     300                     ( $this->type == 'pixi' && $this->version <= 0 ) ||
     301                     ( $this->type == 'iref' && $this->version <= 1 ) ||
     302                     ( $this->type == 'auxC' && $this->version <= 0 );
     303      // Instead of considering this file as invalid, skip unparsable boxes.
     304      if ( !$is_parsable ) {
     305        $this->type = 'unknownversion';
     306      }
     307    }
     308    // print_r( $this ); // Uncomment to print all boxes.
     309    return FOUND;
     310  }
     311}
     312
     313//------------------------------------------------------------------------------
     314
     315class Parser {
     316  private $handle; // Input stream.
     317  private $num_parsed_boxes = 0;
     318  private $data_was_skipped = false;
     319  public $features;
     320
     321  function __construct( $handle ) {
     322    $this->handle   = $handle;
     323    $this->features = new Features();
     324  }
     325
     326  /**
     327   * Parses an "ipco" box.
     328   *
     329   * "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth
     330   * and number of channels, and "auxC" is used for alpha.
     331   *
     332   * @param stream  $handle              The resource the box will be parsed from.
     333   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     334   * @return Status                      FOUND on success or an error on failure.
     335   */
     336  private function parse_ipco( $num_remaining_bytes ) {
     337    $box_index = 1; // 1-based index. Used for iterating over properties.
     338    do {
     339      $box    = new Box();
     340      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     341      if ( $status != FOUND ) {
     342        return $status;
     343      }
     344
     345      if ( $box->type == 'ispe' ) {
     346        // See ISO/IEC 23008-12:2017(E) 6.5.3.2
     347        if ( $box->content_size < 8 ) {
     348          return INVALID;
     349        }
     350        if ( !( $data = read( $this->handle, 8 ) ) ) {
     351          return TRUNCATED;
     352        }
     353        $width  = read_big_endian( substr( $data, 0, 4 ), 4 );
     354        $height = read_big_endian( substr( $data, 4, 4 ), 4 );
     355        if ( $width == 0 || $height == 0 ) {
     356          return INVALID;
     357        }
     358        if ( count( $this->features->dim_props ) <= MAX_FEATURES &&
     359             $box_index <= MAX_VALUE ) {
     360          $dim_prop_count = count( $this->features->dim_props );
     361          $this->features->dim_props[$dim_prop_count]                 = new Dim_Prop();
     362          $this->features->dim_props[$dim_prop_count]->property_index = $box_index;
     363          $this->features->dim_props[$dim_prop_count]->width          = $width;
     364          $this->features->dim_props[$dim_prop_count]->height         = $height;
     365        } else {
     366          $this->data_was_skipped = true;
     367        }
     368        if ( !skip( $this->handle, $box->content_size - 8 ) ) {
     369          return TRUNCATED;
     370        }
     371      } else if ( $box->type == 'pixi' ) {
     372        // See ISO/IEC 23008-12:2017(E) 6.5.6.2
     373        if ( $box->content_size < 1 ) {
     374          return INVALID;
     375        }
     376        if ( !( $data = read( $this->handle, 1 ) ) ) {
     377          return TRUNCATED;
     378        }
     379        $num_channels = read_big_endian( $data, 1 );
     380        if ( $num_channels < 1 ) {
     381          return INVALID;
     382        }
     383        if ( $box->content_size < 1 + $num_channels ) {
     384          return INVALID;
     385        }
     386        if ( !( $data = read( $this->handle, 1 ) ) ) {
     387          return TRUNCATED;
     388        }
     389        $bit_depth = read_big_endian( $data, 1 );
     390        if ( $bit_depth < 1 ) {
     391          return INVALID;
     392        }
     393        for ( $i = 1; $i < $num_channels; ++$i ) {
     394          if ( !( $data = read( $this->handle, 1 ) ) ) {
     395            return TRUNCATED;
     396          }
     397          // Bit depth should be the same for all channels.
     398          if ( read_big_endian( $data, 1 ) != $bit_depth ) {
     399            return INVALID;
     400          }
     401          if ( $i > 32 ) {
     402            return ABORTED; // Be reasonable.
     403          }
     404        }
     405        if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
     406             $box_index <= MAX_VALUE && $bit_depth <= MAX_VALUE &&
     407             $num_channels <= MAX_VALUE ) {
     408          $chan_prop_count = count( $this->features->chan_props );
     409          $this->features->chan_props[$chan_prop_count]                 = new Chan_Prop();
     410          $this->features->chan_props[$chan_prop_count]->property_index = $box_index;
     411          $this->features->chan_props[$chan_prop_count]->bit_depth      = $bit_depth;
     412          $this->features->chan_props[$chan_prop_count]->num_channels   = $num_channels;
     413        } else {
     414          $this->data_was_skipped = true;
     415        }
     416        if ( !skip( $this->handle, $box->content_size - ( 1 + $num_channels ) ) ) {
     417          return TRUNCATED;
     418        }
     419      } else if ( $box->type == 'av1C' ) {
     420        // See AV1 Codec ISO Media File Format Binding 2.3.1
     421        // at https://aomediacodec.github.io/av1-isobmff/#av1c
     422        // Only parse the necessary third byte. Assume that the others are valid.
     423        if ( $box->content_size < 3 ) {
     424          return INVALID;
     425        }
     426        if ( !( $data = read( $this->handle, 3 ) ) ) {
     427          return TRUNCATED;
     428        }
     429        $byte          = read_big_endian( substr( $data, 2, 1 ), 1 );
     430        $high_bitdepth = ( $byte & 0x40 ) != 0;
     431        $twelve_bit    = ( $byte & 0x20 ) != 0;
     432        $monochrome    = ( $byte & 0x10 ) != 0;
     433        if ( $twelve_bit && !$high_bitdepth ) {
     434            return INVALID;
     435        }
     436        if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
     437             $box_index <= MAX_VALUE ) {
     438          $chan_prop_count = count( $this->features->chan_props );
     439          $this->features->chan_props[$chan_prop_count]                 = new Chan_Prop();
     440          $this->features->chan_props[$chan_prop_count]->property_index = $box_index;
     441          $this->features->chan_props[$chan_prop_count]->bit_depth      =
     442              $high_bitdepth ? $twelve_bit ? 12 : 10 : 8;
     443          $this->features->chan_props[$chan_prop_count]->num_channels   = $monochrome ? 1 : 3;
     444        } else {
     445          $this->data_was_skipped = true;
     446        }
     447        if ( !skip( $this->handle, $box->content_size - 3 ) ) {
     448          return TRUNCATED;
     449        }
     450      } else if ( $box->type == 'auxC' ) {
     451        // See AV1 Image File Format (AVIF) 4
     452        // at https://aomediacodec.github.io/av1-avif/#auxiliary-images
     453        $kAlphaStr       = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha\0";
     454        $kAlphaStrLength = 44; // Includes terminating character.
     455        if ( $box->content_size >= $kAlphaStrLength ) {
     456          if ( !( $data = read( $this->handle, $kAlphaStrLength ) ) ) {
     457            return TRUNCATED;
     458          }
     459          if ( substr( $data, 0, $kAlphaStrLength ) == $kAlphaStr ) {
     460            // Note: It is unlikely but it is possible that this alpha plane does
     461            //       not belong to the primary item or a tile. Ignore this issue.
     462            $this->features->has_alpha = true;
     463          }
     464          if ( !skip( $this->handle, $box->content_size - $kAlphaStrLength ) ) {
     465            return TRUNCATED;
     466          }
     467        } else {
     468          if ( !skip( $this->handle, $box->content_size ) ) {
     469            return TRUNCATED;
     470          }
     471        }
     472      } else {
     473        if ( !skip( $this->handle, $box->content_size ) ) {
     474          return TRUNCATED;
     475        }
     476      }
     477      ++$box_index;
     478      $num_remaining_bytes -= $box->size;
     479    } while ( $num_remaining_bytes > 0 );
     480    return NOT_FOUND;
     481  }
     482
     483  /**
     484   * Parses an "iprp" box.
     485   *
     486   * The "ipco" box contain the properties which are linked to items by the "ipma" box.
     487   *
     488   * @param stream  $handle              The resource the box will be parsed from.
     489   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     490   * @return Status                      FOUND on success or an error on failure.
     491   */
     492  private function parse_iprp( $num_remaining_bytes ) {
     493    do {
     494      $box    = new Box();
     495      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     496      if ( $status != FOUND ) {
     497        return $status;
     498      }
     499
     500      if ( $box->type == 'ipco' ) {
     501        $status = $this->parse_ipco( $box->content_size );
     502        if ( $status != NOT_FOUND ) {
     503          return $status;
     504        }
     505      } else if ( $box->type == 'ipma' ) {
     506        // See ISO/IEC 23008-12:2017(E) 9.3.2
     507        $num_read_bytes = 4;
     508        if ( $box->content_size < $num_read_bytes ) {
     509          return INVALID;
     510        }
     511        if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
     512          return TRUNCATED;
     513        }
     514        $entry_count        = read_big_endian( $data, 4 );
     515        $id_num_bytes       = ( $box->version < 1 ) ? 2 : 4;
     516        $index_num_bytes    = ( $box->flags & 1 ) ? 2 : 1;
     517        $essential_bit_mask = ( $box->flags & 1 ) ? 0x8000 : 0x80;
     518
     519        for ( $entry = 0; $entry < $entry_count; ++$entry ) {
     520          if ( $entry >= MAX_PROPS ||
     521               count( $this->features->props ) >= MAX_PROPS ) {
     522            $this->data_was_skipped = true;
     523            break;
     524          }
     525          $num_read_bytes += $id_num_bytes + 1;
     526          if ( $box->content_size < $num_read_bytes ) {
     527            return INVALID;
     528          }
     529          if ( !( $data = read( $this->handle, $id_num_bytes + 1 ) ) ) {
     530            return TRUNCATED;
     531          }
     532          $item_id           = read_big_endian(
     533              substr( $data, 0, $id_num_bytes ), $id_num_bytes );
     534          $association_count = read_big_endian(
     535              substr( $data, $id_num_bytes, 1 ), 1 );
     536
     537          for ( $property = 0; $property < $association_count; ++$property ) {
     538            if ( $property >= MAX_PROPS ||
     539                 count( $this->features->props ) >= MAX_PROPS ) {
     540              $this->data_was_skipped = true;
     541              break;
     542            }
     543            $num_read_bytes += $index_num_bytes;
     544            if ( $box->content_size < $num_read_bytes ) {
     545              return INVALID;
     546            }
     547            if ( !( $data = read( $this->handle, $index_num_bytes ) ) ) {
     548              return TRUNCATED;
     549            }
     550            $value          = read_big_endian( $data, $index_num_bytes );
     551            // $essential = ($value & $essential_bit_mask);  // Unused.
     552            $property_index = ( $value & ~$essential_bit_mask );
     553            if ( $property_index <= MAX_VALUE && $item_id <= MAX_VALUE ) {
     554              $prop_count = count( $this->features->props );
     555              $this->features->props[$prop_count]                 = new Prop();
     556              $this->features->props[$prop_count]->property_index = $property_index;
     557              $this->features->props[$prop_count]->item_id        = $item_id;
     558            } else {
     559              $this->data_was_skipped = true;
     560            }
     561          }
     562          if ( $property < $association_count ) {
     563            break; // Do not read garbage.
     564          }
     565        }
     566
     567        // If all features are available now, do not look further.
     568        $status = $this->features->get_primary_item_features();
     569        if ( $status != NOT_FOUND ) {
     570          return $status;
     571        }
     572
     573        // Mostly if 'data_was_skipped'.
     574        if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
     575          return TRUNCATED;
     576        }
     577      } else {
     578        if ( !skip( $this->handle, $box->content_size ) ) {
     579          return TRUNCATED;
     580        }
     581      }
     582      $num_remaining_bytes -= $box->size;
     583    } while ( $num_remaining_bytes > 0 );
     584    return NOT_FOUND;
     585  }
     586
     587  /**
     588   * Parses an "iref" box.
     589   *
     590   * The "dimg" boxes contain links between tiles and their parent items, which
     591   * can be used to infer bit depth and number of channels for the primary item
     592   * when the latter does not have these properties.
     593   *
     594   * @param stream  $handle              The resource the box will be parsed from.
     595   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     596   * @return Status                      FOUND on success or an error on failure.
     597   */
     598  private function parse_iref( $num_remaining_bytes ) {
     599    do {
     600      $box    = new Box();
     601      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     602      if ( $status != FOUND ) {
     603        return $status;
     604      }
     605
     606      if ( $box->type == 'dimg' ) {
     607        // See ISO/IEC 14496-12:2015(E) 8.11.12.2
     608        $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
     609        $num_read_bytes   = $num_bytes_per_id + 2;
     610        if ( $box->content_size < $num_read_bytes ) {
     611          return INVALID;
     612        }
     613        if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
     614          return TRUNCATED;
     615        }
     616        $from_item_id    = read_big_endian( $data, $num_bytes_per_id );
     617        $reference_count = read_big_endian( substr( $data, $num_bytes_per_id, 2 ), 2 );
     618
     619        for ( $i = 0; $i < $reference_count; ++$i ) {
     620          if ( $i >= MAX_TILES ) {
     621            $this->data_was_skipped = true;
     622            break;
     623          }
     624          $num_read_bytes += $num_bytes_per_id;
     625          if ( $box->content_size < $num_read_bytes ) {
     626            return INVALID;
     627          }
     628          if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
     629            return TRUNCATED;
     630          }
     631          $to_item_id = read_big_endian( $data, $num_bytes_per_id );
     632          $tile_count = count( $this->features->tiles );
     633          if ( $from_item_id <= MAX_VALUE && $to_item_id <= MAX_VALUE &&
     634               $tile_count < MAX_TILES ) {
     635            $this->features->tiles[$tile_count]                 = new Tile();
     636            $this->features->tiles[$tile_count]->tile_item_id   = $to_item_id;
     637            $this->features->tiles[$tile_count]->parent_item_id = $from_item_id;
     638          } else {
     639            $this->data_was_skipped = true;
     640          }
     641        }
     642
     643        // If all features are available now, do not look further.
     644        $status = $this->features->get_primary_item_features();
     645        if ( $status != NOT_FOUND ) {
     646          return $status;
     647        }
     648
     649        // Mostly if 'data_was_skipped'.
     650        if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
     651          return TRUNCATED;
     652        }
     653      } else {
     654        if ( !skip( $this->handle, $box->content_size ) ) {
     655          return TRUNCATED;
     656        }
     657      }
     658      $num_remaining_bytes -= $box->size;
     659    } while ( $num_remaining_bytes > 0 );
     660    return NOT_FOUND;
     661  }
     662
     663  /**
     664   * Parses a "meta" box.
     665   *
     666   * It looks for the primary item ID in the "pitm" box and recurses into other boxes
     667   * to find its features.
     668   *
     669   * @param stream  $handle              The resource the box will be parsed from.
     670   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     671   * @return Status                      FOUND on success or an error on failure.
     672   */
     673  private function parse_meta( $num_remaining_bytes ) {
     674    do {
     675      $box    = new Box();
     676      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     677      if ( $status != FOUND ) {
     678        return $status;
     679      }
     680
     681      if ( $box->type == 'pitm' ) {
     682        // See ISO/IEC 14496-12:2015(E) 8.11.4.2
     683        $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
     684        if ( $num_bytes_per_id > $num_remaining_bytes ) {
     685          return INVALID;
     686        }
     687        if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
     688          return TRUNCATED;
     689        }
     690        $primary_item_id = read_big_endian( $data, $num_bytes_per_id );
     691        if ( $primary_item_id > MAX_VALUE ) {
     692          return ABORTED;
     693        }
     694        $this->features->has_primary_item = true;
     695        $this->features->primary_item_id  = $primary_item_id;
     696        if ( !skip( $this->handle, $box->content_size - $num_bytes_per_id ) ) {
     697          return TRUNCATED;
     698        }
     699      } else if ( $box->type == 'iprp' ) {
     700        $status = $this->parse_iprp( $box->content_size );
     701        if ( $status != NOT_FOUND ) {
     702          return $status;
     703        }
     704      } else if ( $box->type == 'iref' ) {
     705        $status = $this->parse_iref( $box->content_size );
     706        if ( $status != NOT_FOUND ) {
     707          return $status;
     708        }
     709      } else {
     710        if ( !skip( $this->handle, $box->content_size ) ) {
     711          return TRUNCATED;
     712        }
     713      }
     714      $num_remaining_bytes -= $box->size;
     715    } while ( $num_remaining_bytes != 0 );
     716    // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta".
     717    return INVALID;
     718  }
     719
     720  /**
     721   * Parses a file stream.
     722   *
     723   * The file type is checked through the "ftyp" box.
     724   *
     725   * @return bool True if the input stream is an AVIF bitstream or false.
     726   */
     727  public function parse_ftyp() {
     728    $box    = new Box();
     729    $status = $box->parse( $this->handle, $this->num_parsed_boxes );
     730    if ( $status != FOUND ) {
     731      return false;
     732    }
     733
     734    if ( $box->type != 'ftyp' ) {
     735      return false;
     736    }
     737    // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
     738    if ( $box->content_size < 8 ) {
     739      return false;
     740    }
     741    for ( $i = 0; $i + 4 <= $box->content_size; $i += 4 ) {
     742      if ( !( $data = read( $this->handle, 4 ) ) ) {
     743        return false;
     744      }
     745      if ( $i == 4 ) {
     746        continue; // Skip minor_version.
     747      }
     748      if ( substr( $data, 0, 4 ) == 'avif' || substr( $data, 0, 4 ) == 'avis' ) {
     749        return skip( $this->handle, $box->content_size - ( $i + 4 ) );
     750      }
     751      if ( $i > 32 * 4 ) {
     752        return false; // Be reasonable.
     753      }
     754
     755    }
     756    return false; // No AVIF brand no good.
     757  }
     758
     759  /**
     760   * Parses a file stream.
     761   *
     762   * Features are extracted from the "meta" box.
     763   *
     764   * @return bool True if the main features of the primary item were parsed or false.
     765   */
     766  public function parse_file() {
     767    $box = new Box();
     768    while ( $box->parse( $this->handle, $this->num_parsed_boxes ) == FOUND ) {
     769      if ( $box->type === 'meta' ) {
     770        if ( $this->parse_meta( $box->content_size ) != FOUND ) {
     771          return false;
     772        }
     773        return true;
     774      }
     775      if ( !skip( $this->handle, $box->content_size ) ) {
     776        return false;
     777      }
     778    }
     779    return false; // No "meta" no good.
     780  }
     781}
  • src/wp-includes/class-wp-image-editor-gd.php

    diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php
    index de079357fb..23331c7f19 100644
    a b class WP_Image_Editor_GD extends WP_Image_Editor { 
    7171                                return ( $image_types & IMG_GIF ) != 0;
    7272                        case 'image/webp':
    7373                                return ( $image_types & IMG_WEBP ) != 0;
     74                        case 'image/avif':
     75                                return ( $image_types & IMG_AVIF ) != 0;
    7476                }
    7577
    7678                return false;
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    111113                        $this->image = @imagecreatefromstring( $file_contents );
    112114                }
    113115
     116                // AVIF may not work with imagecreatefromstring().
     117                if (
     118                        function_exists( 'imagecreatefromavif' ) &&
     119                        ( 'image/avif' === wp_get_image_mime( $this->file ) )
     120                ) {
     121                        $this->image = @imagecreatefromavif( $this->file );
     122                } else {
     123                        $this->image = @imagecreatefromstring( $file_contents );
     124                }
     125
    114126                if ( ! is_gd_image( $this->image ) ) {
    115127                        return new WP_Error( 'invalid_image', __( 'File is not an image.' ), $this->file );
    116128                }
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    513525                        if ( ! function_exists( 'imagewebp' ) || ! $this->make_image( $filename, 'imagewebp', array( $image, $filename, $this->get_quality() ) ) ) {
    514526                                return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
    515527                        }
     528                } elseif ( 'image/avif' == $mime_type ) {
     529                        if ( ! function_exists( 'imageavif' ) || ! $this->make_image( $filename, 'imageavif', array( $image, $filename, $this->get_quality() ) ) ) {
     530                                return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
     531                        }
    516532                } else {
    517533                        return new WP_Error( 'image_save_error', __( 'Image Editor Save Failed' ) );
    518534                }
    class WP_Image_Editor_GD extends WP_Image_Editor { 
    561577                                if ( function_exists( 'imagewebp' ) ) {
    562578                                        header( 'Content-Type: image/webp' );
    563579                                        return imagewebp( $this->image, null, $this->get_quality() );
     580                                } else {
     581                                        // Fall back to JPEG.
     582                                        header( 'Content-Type: image/jpeg' );
     583                                        return imagejpeg( $this->image, null, $this->get_quality() );
     584                                }
     585                        case 'image/avif':
     586                                if ( function_exists( 'imageavif' ) ) {
     587                                        header( 'Content-Type: image/avif' );
     588                                        return imageavif( $this->image, null, $this->get_quality() );
    564589                                }
    565                                 // Fall back to the default if webp isn't supported.
     590                                // Fall back to JPEG.
    566591                        default:
    567592                                header( 'Content-Type: image/jpeg' );
    568593                                return imagejpeg( $this->image, null, $this->get_quality() );
  • src/wp-includes/class-wp-image-editor-imagick.php

    diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php
    index 03fe0bca69..546ad3e799 100644
    a b class WP_Image_Editor_Imagick extends WP_Image_Editor { 
    219219                                                $this->image->setImageCompressionQuality( $quality );
    220220                                        }
    221221                                        break;
     222                                case 'image/avif':
    222223                                default:
    223224                                        $this->image->setImageCompressionQuality( $quality );
    224225                        }
    class WP_Image_Editor_Imagick extends WP_Image_Editor { 
    256257                        $height = $size['height'];
    257258                }
    258259
     260                /*
     261                 * If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images
     262                 * are properly sized without affecting previous `getImageGeometry` behavior.
     263                 */
     264                if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) {
     265                        $size   = wp_getimagesize( $this->file );
     266                        $width  = $size[0];
     267                        $height = $size[1];
     268                }
     269
    259270                return parent::update_size( $width, $height );
    260271        }
    261272
  • src/wp-includes/class-wp-image-editor.php

    diff --git a/src/wp-includes/class-wp-image-editor.php b/src/wp-includes/class-wp-image-editor.php
    index 3c636dc6ba..6604685a02 100644
    a b abstract class WP_Image_Editor { 
    318318                                $quality = 86;
    319319                                break;
    320320                        case 'image/jpeg':
     321                        case 'image/avif':
    321322                        default:
    322323                                $quality = $this->default_quality;
    323324                }
  • src/wp-includes/class-wp-theme.php

    diff --git a/src/wp-includes/class-wp-theme.php b/src/wp-includes/class-wp-theme.php
    index 09905bee1b..2058a9e557 100644
    a b final class WP_Theme implements ArrayAccess { 
    12631263                        return false;
    12641264                }
    12651265
    1266                 foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp' ) as $ext ) {
     1266                foreach ( array( 'png', 'gif', 'jpg', 'jpeg', 'webp', 'avif' ) as $ext ) {
    12671267                        if ( file_exists( $this->get_stylesheet_directory() . "/screenshot.$ext" ) ) {
    12681268                                $this->cache_add( 'screenshot', 'screenshot.' . $ext );
    12691269                                if ( 'relative' === $uri ) {
  • src/wp-includes/compat.php

    diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php
    index 429c5f92e7..95c4af484d 100644
    a b if ( ! defined( 'IMAGETYPE_WEBP' ) ) { 
    529529if ( ! defined( 'IMG_WEBP' ) ) {
    530530        define( 'IMG_WEBP', IMAGETYPE_WEBP );
    531531}
     532
     533// IMAGETYPE_AVIF constant is only defined in PHP 8.x or later.
     534if ( ! defined( 'IMAGETYPE_AVIF' ) ) {
     535        define( 'IMAGETYPE_AVIF', 19 );
     536}
     537
     538// IMG_AVIF constant is only defined in PHP 8.x or later.
     539if ( ! defined( 'IMG_AVIF' ) ) {
     540        define( 'IMG_AVIF', IMAGETYPE_AVIF );
     541}
  • src/wp-includes/customize/class-wp-customize-media-control.php

    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 f636892925..3bdc4e2dc1 100644
    a b class WP_Customize_Media_Control extends WP_Customize_Control { 
    9393                                 * Note that the default value must be a URL, NOT an attachment ID.
    9494                                 */
    9595                                $ext  = substr( $this->setting->default, -3 );
    96                                 $type = in_array( $ext, array( 'jpg', 'png', 'gif', 'bmp', 'webp' ), true ) ? 'image' : 'document';
     96                                $type = in_array( $ext, array( 'jpg', 'png', 'gif', 'bmp', 'webp', 'avif' ), true ) ? 'image' : 'document';
    9797
    9898                                $default_attachment = array(
    9999                                        'id'    => 1,
  • src/wp-includes/deprecated.php

    diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php
    index 1fd62ae16e..45b4f89a9e 100644
    a b function gd_edit_image_support($mime_type) { 
    33363336                                return (imagetypes() & IMG_GIF) != 0;
    33373337                        case 'image/webp':
    33383338                                return (imagetypes() & IMG_WEBP) != 0;
    3339                 }
     3339                        case 'image/avif':
     3340                                return (imagetypes() & IMG_AVIF) != 0;
     3341                        }
    33403342        } else {
    33413343                switch( $mime_type ) {
    33423344                        case 'image/jpeg':
    function gd_edit_image_support($mime_type) { 
    33473349                                return function_exists('imagecreatefromgif');
    33483350                        case 'image/webp':
    33493351                                return function_exists('imagecreatefromwebp');
     3352                        case 'image/avif':
     3353                                return function_exists('imagecreatefromavif');
    33503354                }
    33513355        }
    33523356        return false;
  • src/wp-includes/formatting.php

    diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php
    index 05b103e6f7..ce8d035420 100644
    a b function translate_smiley( $matches ) { 
    34643464
    34653465        $matches    = array();
    34663466        $ext        = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false;
    3467         $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' );
     3467        $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' );
    34683468
    34693469        // Don't convert smilies that aren't images - they're probably emoji.
    34703470        if ( ! in_array( $ext, $image_exts, true ) ) {
  • src/wp-includes/functions.php

    diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php
    index 94995206f1..17319c7f46 100644
    a b function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { 
    31173117                                        'image/bmp'  => 'bmp',
    31183118                                        'image/tiff' => 'tif',
    31193119                                        'image/webp' => 'webp',
     3120                                        'image/avif' => 'avif',
    31203121                                )
    31213122                        );
    31223123
    function wp_check_filetype_and_ext( $file, $filename, $mimes = null ) { 
    32953296 *
    32963297 * @since 4.7.1
    32973298 * @since 5.8.0 Added support for WebP images.
     3299 * @since 6.5.0 Added support for AVIF images.
    32983300 *
    32993301 * @param string $file Full path to the file.
    33003302 * @return string|false The actual mime type or false if the type cannot be determined.
    function wp_get_image_mime( $file ) { 
    33493351                ) {
    33503352                        $mime = 'image/webp';
    33513353                }
     3354
     3355                /**
     3356                 * Add AVIF fallback detection when image library doesn't support AVIF.
     3357                 *
     3358                 * Detection based on section 4.3.1 File-type box definition of the ISO/IEC 14496-12
     3359                 * specification and the AV1-AVIF spec, see https://aomediacodec.github.io/av1-avif/v1.1.0.html#brands.
     3360                 */
     3361
     3362                 // Divide the header string into 4 byte groups.
     3363                $magic = str_split( $magic, 8 );
     3364
     3365                if (
     3366                        isset( $magic[1] ) &&
     3367                        isset( $magic[2] ) &&
     3368                        'ftyp' === hex2bin( $magic[1] ) &&
     3369                        ( 'avif' === hex2bin( $magic[2] ) || 'avis' === hex2bin( $magic[2] ) )
     3370                ) {
     3371                        $mime = 'image/avif';
     3372                }
    33523373        } catch ( Exception $e ) {
    33533374                $mime = false;
    33543375        }
    function wp_get_mime_types() { 
    33883409                        'bmp'                          => 'image/bmp',
    33893410                        'tiff|tif'                     => 'image/tiff',
    33903411                        'webp'                         => 'image/webp',
     3412                        'avif'                         => 'image/avif',
    33913413                        'ico'                          => 'image/x-icon',
    33923414                        'heic'                         => 'image/heic',
    33933415                        // Video formats.
    function wp_get_ext_types() { 
    35093531        return apply_filters(
    35103532                'ext2type',
    35113533                array(
    3512                         'image'       => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp' ),
     3534                        'image'       => array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'bmp', 'tif', 'tiff', 'ico', 'heic', 'webp', 'avif' ),
    35133535                        'audio'       => array( 'aac', 'ac3', 'aif', 'aiff', 'flac', 'm3a', 'm4a', 'm4b', 'mka', 'mp1', 'mp2', 'mp3', 'ogg', 'oga', 'ram', 'wav', 'wma' ),
    35143536                        'video'       => array( '3g2', '3gp', '3gpp', 'asf', 'avi', 'divx', 'dv', 'flv', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mpv', 'ogm', 'ogv', 'qt', 'rm', 'vob', 'wmv' ),
    35153537                        'document'    => array( 'doc', 'docx', 'docm', 'dotm', 'odt', 'pages', 'pdf', 'xps', 'oxps', 'rtf', 'wp', 'wpd', 'psd', 'xcf' ),
  • src/wp-includes/media.php

    diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php
    index 38ec2213b7..69a6d53299 100644
    a b function _wp_image_editor_choose( $args = array() ) { 
    41004100        require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
    41014101        require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
    41024102        require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
     4103        require_once ABSPATH . WPINC . '/class-avif-info.php';
    41034104        /**
    41044105         * Filters the list of image editing library classes.
    41054106         *
    function wp_plupload_default_settings() { 
    42044205                $defaults['webp_upload_error'] = true;
    42054206        }
    42064207
     4208        // Check if AVIF images can be edited.
     4209        if ( ! wp_image_editor_supports( array( 'mime_type' => 'image/avif' ) ) ) {
     4210                $defaults['avif_upload_error'] = true;
     4211        }
     4212
    42074213        /**
    42084214         * Filters the Plupload default settings.
    42094215         *
    function wp_show_heic_upload_error( $plupload_settings ) { 
    54805486 *
    54815487 * @since 5.7.0
    54825488 * @since 5.8.0 Added support for WebP images.
     5489 * @since 6.5.0 Added support for AVIF images.
    54835490 *
    54845491 * @param string $filename   The file path.
    54855492 * @param array  $image_info Optional. Extended image information (passed by reference).
    function wp_getimagesize( $filename, array &$image_info = null ) { 
    55125519                }
    55135520        }
    55145521
    5515         if ( false !== $info ) {
     5522        if (
     5523                ! empty( $info ) &&
     5524                // Some PHP versions return 0x0 sizes from `getimagesize` for unrecognized image formats, including AVIFs.
     5525                ! ( empty( $info[0] ) && empty( $info[1] ) )
     5526        ) {
    55165527                return $info;
    55175528        }
    55185529
    function wp_getimagesize( $filename, array &$image_info = null ) { 
    55415552                }
    55425553        }
    55435554
     5555        // For PHP versions that don't support AVIF images, extract the image size info from the file headers.
     5556        if ( 'image/avif' === wp_get_image_mime( $filename ) ) {
     5557                $avif_info = wp_get_avif_info( $filename );
     5558
     5559                $width  = $avif_info['width'];
     5560                $height = $avif_info['height'];
     5561
     5562                // Mimic the native return format.
     5563                if ( $width && $height ) {
     5564                        return array(
     5565                                $width,
     5566                                $height,
     5567                                IMAGETYPE_AVIF,
     5568                                sprintf(
     5569                                        'width="%d" height="%d"',
     5570                                        $width,
     5571                                        $height
     5572                                ),
     5573                                'mime' => 'image/avif',
     5574                        );
     5575                }
     5576        }
     5577
    55445578        // The image could not be parsed.
    55455579        return false;
    55465580}
    55475581
     5582/**
     5583 * Extracts meta information about an AVIF file: width, height, bit depth, and number of channels.
     5584 *
     5585 * @since 6.5.0
     5586 *
     5587 * @param string $filename Path to an AVIF file.
     5588 * @return array {
     5589 *    An array of AVIF image information.
     5590 *
     5591 *    @type int|false $width        Image width on success, false on failure.
     5592 *    @type int|false $height       Image height on success, false on failure.
     5593 *    @type int|false $bit_depth    Image bit depth on success, false on failure.
     5594 *    @type int|false $num_channels Image number of channels on success, false on failure.
     5595 * }
     5596 */
     5597function wp_get_avif_info( $filename ) {
     5598        $results = array(
     5599                'width'        => false,
     5600                'height'       => false,
     5601                'bit_depth'    => false,
     5602                'num_channels' => false,
     5603        );
     5604
     5605        if ( 'image/avif' !== wp_get_image_mime( $filename ) ) {
     5606                return $results;
     5607        }
     5608
     5609        // Parse the file using libavifinfo's PHP implementation.
     5610        require_once ABSPATH . WPINC . '/class-avif-info.php';
     5611
     5612        $handle = fopen( $filename, 'rb' );
     5613        if ( $handle ) {
     5614                $parser  = new Avifinfo\Parser( $handle );
     5615                $success = $parser->parse_ftyp() && $parser->parse_file();
     5616                fclose( $handle );
     5617                if ( $success ) {
     5618                        $results = $parser->features->primary_item_features;
     5619                }
     5620        }
     5621        return $results;
     5622}
     5623
    55485624/**
    55495625 * Extracts meta information about a WebP file: width, height, and type.
    55505626 *
  • src/wp-includes/post.php

    diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php
    index 69a6362552..001ccaf7d0 100644
    a b function wp_attachment_is( $type, $post = null ) { 
    67006700
    67016701        switch ( $type ) {
    67026702                case 'image':
    6703                         $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp' );
     6703                        $image_exts = array( 'jpg', 'jpeg', 'jpe', 'gif', 'png', 'webp', 'avif' );
    67046704                        return in_array( $ext, $image_exts, true );
    67056705
    67066706                case 'audio':
  • src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    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 d6bba7a924..8a26a79a67 100644
    a b class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { 
    456456                        );
    457457                }
    458458
    459                 $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
     459                $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' );
    460460                $mime_type       = get_post_mime_type( $attachment_id );
    461461                if ( ! in_array( $mime_type, $supported_types, true ) ) {
    462462                        return new WP_Error(
  • tests/phpunit/tests/functions.php

    diff --git a/tests/phpunit/data/images/avif-animated.avif b/tests/phpunit/data/images/avif-animated.avif
    new file mode 100644
    index 0000000000..6d6a34a730
    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 0000000000..7eb2d5ce68
    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 0000000000..0aba41c1bf
    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 0000000000..8165f9ff46
    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 0000000000..fa301f5898
    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 0000000000..6a2403f110
    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 3c2e1101ad..abef68f75b 100644
    a b class Tests_Functions extends WP_UnitTestCase { 
    13701370                                DIR_TESTDATA . '/uploads/dashicons.woff',
    13711371                                false,
    13721372                        ),
     1373                        // Animated AVIF.
     1374                        array(
     1375                                DIR_TESTDATA . '/images/avif-animated.avif',
     1376                                'image/avif',
     1377                        ),
     1378                        // Lossless AVIF.
     1379                        array(
     1380                                DIR_TESTDATA . '/images/avif-lossless.avif',
     1381                                'image/avif',
     1382                        ),
     1383                        // Lossy AVIF.
     1384                        array(
     1385                                DIR_TESTDATA . '/images/avif-lossy.avif',
     1386                                'image/avif',
     1387                        ),
     1388                        // Transparent AVIF.
     1389                        array(
     1390                                DIR_TESTDATA . '/images/avif-transparent.avif',
     1391                                'image/avif',
     1392                        ),
    13731393                );
    13741394
    13751395                return $data;
    class Tests_Functions extends WP_UnitTestCase { 
    14961516                                DIR_TESTDATA . '/uploads/dashicons.woff',
    14971517                                false,
    14981518                        ),
     1519                        // Animated AVIF.
     1520                        array(
     1521                                DIR_TESTDATA . '/images/avif-animated.avif',
     1522                                array(
     1523                                        150,
     1524                                        150,
     1525                                        IMAGETYPE_AVIF,
     1526                                        'width="150" height="150"',
     1527                                        'mime' => 'image/avif',
     1528                                ),
     1529                        ),
     1530                        // Lossless AVIF.
     1531                        array(
     1532                                DIR_TESTDATA . '/images/avif-lossless.avif',
     1533                                array(
     1534                                        400,
     1535                                        400,
     1536                                        IMAGETYPE_AVIF,
     1537                                        'width="400" height="400"',
     1538                                        'mime' => 'image/avif',
     1539                                ),
     1540                        ),
     1541                        // Lossy AVIF.
     1542                        array(
     1543                                DIR_TESTDATA . '/images/avif-lossy.avif',
     1544                                array(
     1545                                        400,
     1546                                        400,
     1547                                        IMAGETYPE_AVIF,
     1548                                        'width="400" height="400"',
     1549                                        'mime' => 'image/avif',
     1550                                ),
     1551                        ),
     1552                        // Transparent AVIF.
     1553                        array(
     1554                                DIR_TESTDATA . '/images/avif-transparent.avif',
     1555                                array(
     1556                                        128,
     1557                                        128,
     1558                                        IMAGETYPE_AVIF,
     1559                                        'width="128" height="128"',
     1560                                        'mime' => 'image/avif',
     1561                                ),
     1562                        ),
    14991563                );
    15001564
    15011565                return $data;
  • tests/phpunit/tests/image/editor.php

    diff --git a/tests/phpunit/tests/image/editor.php b/tests/phpunit/tests/image/editor.php
    index bd54b803e2..5e857bf472 100644
    a b class Tests_Image_Editor extends WP_Image_UnitTestCase { 
    292292         *
    293293         */
    294294        public function test_wp_get_webp_info( $file, $expected ) {
    295                 $editor = wp_get_image_editor( $file );
    296 
    297                 if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/webp' ) ) {
    298                         $this->markTestSkipped( sprintf( 'No WebP support in the editor engine %s on this system.', $this->editor_engine ) );
    299                 }
    300 
    301295                $file_data = wp_get_webp_info( $file );
    302296                $this->assertSame( $expected, $file_data );
    303297        }
    class Tests_Image_Editor extends WP_Image_UnitTestCase { 
    363357                        ),
    364358                );
    365359        }
     360
     361        /**
     362         * Test wp_get_avif_info.
     363         *
     364         * @ticket 51228
     365         *
     366         * @dataProvider data_wp_get_avif_info
     367         *
     368         * @param string $file     The path to the AVIF file for testing.
     369         * @param array  $expected The expected AVIF file information.
     370         */
     371        public function test_wp_get_avif_info( $file, $expected ) {
     372                $file_data = wp_get_avif_info( $file );
     373                $this->assertSame( $expected, $file_data );
     374        }
     375
     376        /**
     377         * Data provider for test_wp_get_avif_info().
     378         */
     379        public function data_wp_get_avif_info() {
     380                return array(
     381                        // Standard JPEG.
     382                        array(
     383                                DIR_TESTDATA . '/images/test-image.jpg',
     384                                array(
     385                                        'width'        => false,
     386                                        'height'       => false,
     387                                        'bit_depth'    => false,
     388                                        'num_channels' => false,
     389                                ),
     390                        ),
     391                        // Standard GIF.
     392                        array(
     393                                DIR_TESTDATA . '/images/test-image.gif',
     394                                array(
     395                                        'width'        => false,
     396                                        'height'       => false,
     397                                        'bit_depth'    => false,
     398                                        'num_channels' => false,
     399                                ),
     400                        ),
     401                        // Animated AVIF.
     402                        array(
     403                                DIR_TESTDATA . '/images/avif-animated.avif',
     404                                array(
     405                                        'width'        => 150,
     406                                        'height'       => 150,
     407                                        'bit_depth'    => 8,
     408                                        'num_channels' => 4,
     409                                ),
     410                        ),
     411                        // Lossless AVIF.
     412                        array(
     413                                DIR_TESTDATA . '/images/avif-lossless.avif',
     414                                array(
     415                                        'width'        => 400,
     416                                        'height'       => 400,
     417                                        'bit_depth'    => 8,
     418                                        'num_channels' => 3,
     419                                ),
     420                        ),
     421                        // Lossy AVIF.
     422                        array(
     423                                DIR_TESTDATA . '/images/avif-lossy.avif',
     424                                array(
     425                                        'width'        => 400,
     426                                        'height'       => 400,
     427                                        'bit_depth'    => 8,
     428                                        'num_channels' => 3,
     429                                ),
     430                        ),
     431                        // Transparent AVIF.
     432                        array(
     433                                DIR_TESTDATA . '/images/avif-transparent.avif',
     434                                array(
     435                                        'width'        => 128,
     436                                        'height'       => 128,
     437                                        'bit_depth'    => 12,
     438                                        'num_channels' => 4,
     439                                ),
     440                        ),
     441                        array(
     442                                DIR_TESTDATA . '/images/color_grid_alpha_nogrid.avif',
     443                                array(
     444                                        'width'        => 80,
     445                                        'height'       => 80,
     446                                        'bit_depth'    => 8,
     447                                        'num_channels' => 4,
     448                                ),
     449                        ),
     450                        array(
     451                                DIR_TESTDATA . '/images/colors_hdr_p3.avif',
     452                                array(
     453                                        'width'        => 200,
     454                                        'height'       => 200,
     455                                        'bit_depth'    => 10,
     456                                        'num_channels' => 3,
     457                                ),
     458                        ),
     459                );
     460        }
    366461}
  • tests/phpunit/tests/image/functions.php

    diff --git a/tests/phpunit/tests/image/functions.php b/tests/phpunit/tests/image/functions.php
    index f55b164496..56c81a62f0 100644
    a b class Tests_Image_Functions extends WP_UnitTestCase { 
    111111                        'webp-lossless.webp',
    112112                        'webp-lossy.webp',
    113113                        'webp-transparent.webp',
     114                        'avif-animated.avif',
     115                        'avif-lossless.avif',
     116                        'avif-lossy.avif',
     117                        'avif-transparent.avif',
    114118                );
    115119
    116120                return $this->text_array_to_dataprovider( $files );
    class Tests_Image_Functions extends WP_UnitTestCase { 
    186190                        $files[] = 'webp-transparent.webp';
    187191                }
    188192
     193                // Add AVIF images if the image editor supports them.
     194                $file   = DIR_TESTDATA . '/images/avif-lossless.avif';
     195                $editor = wp_get_image_editor( $file );
     196
     197                if ( ! is_wp_error( $editor ) && $editor->supports_mime_type( 'image/avif' ) ) {
     198                        $files[] = 'avif-animated.avif';
     199                        $files[] = 'avif-lossless.avif';
     200                        $files[] = 'avif-lossy.avif';
     201                        $files[] = 'avif-transparent.avif';
     202                }
     203
    189204                return $this->text_array_to_dataprovider( $files );
    190205        }
    191206
  • tests/phpunit/tests/image/resize.php

    diff --git a/tests/phpunit/tests/image/resize.php b/tests/phpunit/tests/image/resize.php
    index 5b302ce295..e82dd3d2e6 100644
    a b abstract class WP_Tests_Image_Resize_UnitTestCase extends WP_Image_UnitTestCase 
    8888                $this->assertSame( IMAGETYPE_WEBP, $type );
    8989        }
    9090
     91        /**
     92         * Test resizing AVIF image.
     93         *
     94         * @ticket 51228
     95         */
     96        public function test_resize_avif() {
     97                $file   = DIR_TESTDATA . '/images/avif-lossy.avif';
     98                $editor = wp_get_image_editor( $file );
     99
     100                // Check if the editor supports the avif mime type.
     101                if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/avif' ) ) {
     102                        $this->markTestSkipped( sprintf( 'No AVIF support in the editor engine %s on this system.', $this->editor_engine ) );
     103                }
     104
     105                $image = $this->resize_helper( $file, 25, 25 );
     106
     107                list( $w, $h, $type ) = wp_getimagesize( $image );
     108
     109                unlink( $image );
     110
     111                $this->assertSame( 'avif-lossy-25x25.avif', wp_basename( $image ) );
     112                $this->assertSame( 25, $w );
     113                $this->assertSame( 25, $h );
     114                $this->assertSame( IMAGETYPE_AVIF, $type );
     115        }
     116
    91117        public function test_resize_larger() {
    92118                // image_resize() should refuse to make an image larger.
    93119                $image = $this->resize_helper( DIR_TESTDATA . '/images/test-image.jpg', 100, 100 );