Make WordPress Core

Ticket #51228: 51228.diff

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

    diff --git a/phpcs.xml.dist b/phpcs.xml.dist
    index ccb0430321..20f3a20539 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-wp-block-parser\.php</exclude-pattern>
     63        <exclude-pattern>/src/wp-includes/class-avif-info\.php</exclude-pattern>
    6264        <exclude-pattern>/src/wp-includes/deprecated\.php</exclude-pattern>
    6365        <exclude-pattern>/src/wp-includes/ms-deprecated\.php</exclude-pattern>
    6466        <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 d339af3b17..871caaa03b 100644
    a b We hope you enjoy your new site. Thanks! 
    12521252                'png',
    12531253                'gif',
    12541254                'webp',
     1255                'avif',
    12551256                // Video.
    12561257                'mov',
    12571258                '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..72405c326c
    - +  
     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 b496868.
     13 */
     14
     15namespace Avifinfo;
     16
     17const FOUND     = 0; // Input correctly parsed and information retrieved.
     18const NOT_FOUND = 1; // Input correctly parsed but information is missing or elsewhere.
     19const TRUNCATED = 2; // Input correctly parsed until missing bytes to continue.
     20const ABORTED   = 3; // Input correctly parsed until stopped to avoid timeout or crash.
     21const INVALID   = 4; // Input incorrectly parsed.
     22
     23const MAX_SIZE      = 4294967295; // Unlikely to be insufficient to parse AVIF headers.
     24const MAX_NUM_BOXES = 4096;       // Be reasonable. Avoid timeouts and out-of-memory.
     25const MAX_VALUE     = 255;
     26const MAX_TILES     = 16;
     27const MAX_PROPS     = 32;
     28const MAX_FEATURES  = 8;
     29const UNDEFINED     = 0;          // Value was not yet parsed.
     30
     31/**
     32 * Reads an unsigned integer with most significant bits first.
     33 *
     34 * @param binary string $input     Must be at least $num_bytes-long.
     35 * @param int           $num_bytes Number of parsed bytes.
     36 * @return int                     Value.
     37 */
     38function read_big_endian( $input, $num_bytes ) {
     39  if ( $num_bytes == 1 ) {
     40    return unpack( 'C', $input ) [1];
     41  } else if ( $num_bytes == 2 ) {
     42    return unpack( 'n', $input ) [1];
     43  } else if ( $num_bytes == 3 ) {
     44    $bytes = unpack( 'C3', $input );
     45    return ( $bytes[1] << 16 ) | ( $bytes[2] << 8 ) | $bytes[3];
     46  } else { // $num_bytes is 4
     47    // This might fail to read unsigned values >= 2^31 on 32-bit systems.
     48    // See https://www.php.net/manual/en/function.unpack.php#106041
     49    return unpack( 'N', $input ) [1];
     50  }
     51}
     52
     53/**
     54 * Reads bytes and advances the stream position by the same count.
     55 *
     56 * @param stream               $handle    Bytes will be read from this resource.
     57 * @param int                  $num_bytes Number of bytes read. Must be greater than 0.
     58 * @return binary string|false            The raw bytes or false on failure.
     59 */
     60function read( $handle, $num_bytes ) {
     61  $data = fread( $handle, $num_bytes );
     62  return ( $data !== false && strlen( $data ) >= $num_bytes ) ? $data : false;
     63}
     64
     65/**
     66 * Advances the stream position by the given offset.
     67 *
     68 * @param stream $handle    Bytes will be skipped from this resource.
     69 * @param int    $num_bytes Number of skipped bytes. Can be 0.
     70 * @return bool             True on success or false on failure.
     71 */
     72// Skips 'num_bytes' from the 'stream'. 'num_bytes' can be zero.
     73function skip( $handle, $num_bytes ) {
     74  return ( fseek( $handle, $num_bytes, SEEK_CUR ) == 0 );
     75}
     76
     77//------------------------------------------------------------------------------
     78// Features are parsed into temporary property associations.
     79
     80class Tile { // Tile item id <-> parent item id associations.
     81  public $tile_item_id;
     82  public $parent_item_id;
     83}
     84
     85class Prop { // Property index <-> item id associations.
     86  public $property_index;
     87  public $item_id;
     88}
     89
     90class Dim_Prop { // Property <-> features associations.
     91  public $property_index;
     92  public $width;
     93  public $height;
     94}
     95
     96class Chan_Prop { // Property <-> features associations.
     97  public $property_index;
     98  public $bit_depth;
     99  public $num_channels;
     100}
     101
     102class Features {
     103  public $has_primary_item = false; // True if "pitm" was parsed.
     104  public $has_alpha = false; // True if an alpha "auxC" was parsed.
     105  public $primary_item_id;
     106  public $primary_item_features = array( // Deduced from the data below.
     107    'width'        => UNDEFINED, // In number of pixels.
     108    'height'       => UNDEFINED, // Ignores mirror and rotation.
     109    'bit_depth'    => UNDEFINED, // Likely 8, 10 or 12 bits per channel per pixel.
     110    'num_channels' => UNDEFINED  // Likely 1, 2, 3 or 4 channels:
     111                                          //   (1 monochrome or 3 colors) + (0 or 1 alpha)
     112  );
     113
     114  public $tiles = array(); // Tile[]
     115  public $props = array(); // Prop[]
     116  public $dim_props = array(); // Dim_Prop[]
     117  public $chan_props = array(); // Chan_Prop[]
     118
     119  /**
     120   * Binds the width, height, bit depth and number of channels from stored internal features.
     121   *
     122   * @param int     $target_item_id Id of the item whose features will be bound.
     123   * @param int     $tile_depth     Maximum recursion to search within tile-parent relations.
     124   * @return Status                 FOUND on success or NOT_FOUND on failure.
     125   */
     126  private function get_item_features( $target_item_id, $tile_depth ) {
     127    foreach ( $this->props as $prop ) {
     128      if ( $prop->item_id != $target_item_id ) {
     129        continue;
     130      }
     131
     132      // Retrieve the width and height of the primary item if not already done.
     133      if ( $target_item_id == $this->primary_item_id &&
     134           ( $this->primary_item_features['width'] == UNDEFINED ||
     135             $this->primary_item_features['height'] == UNDEFINED ) ) {
     136        foreach ( $this->dim_props as $dim_prop ) {
     137          if ( $dim_prop->property_index != $prop->property_index ) {
     138            continue;
     139          }
     140          $this->primary_item_features['width']  = $dim_prop->width;
     141          $this->primary_item_features['height'] = $dim_prop->height;
     142          if ( $this->primary_item_features['bit_depth'] != UNDEFINED &&
     143               $this->primary_item_features['num_channels'] != UNDEFINED ) {
     144            return FOUND;
     145          }
     146          break;
     147        }
     148      }
     149      // Retrieve the bit depth and number of channels of the target item if not
     150      // already done.
     151      if ( $this->primary_item_features['bit_depth'] == UNDEFINED ||
     152           $this->primary_item_features['num_channels'] == UNDEFINED ) {
     153        foreach ( $this->chan_props as $chan_prop ) {
     154          if ( $chan_prop->property_index != $prop->property_index ) {
     155            continue;
     156          }
     157          $this->primary_item_features['bit_depth']    = $chan_prop->bit_depth;
     158          $this->primary_item_features['num_channels'] = $chan_prop->num_channels;
     159          if ( $this->primary_item_features['width'] != UNDEFINED &&
     160              $this->primary_item_features['height'] != UNDEFINED ) {
     161            return FOUND;
     162          }
     163          break;
     164        }
     165      }
     166    }
     167
     168    // Check for the bit_depth and num_channels in a tile if not yet found.
     169    if ( $tile_depth < 3 ) {
     170      foreach ( $this->tiles as $tile ) {
     171        if ( $tile->parent_item_id != $target_item_id ) {
     172          continue;
     173        }
     174        $status = get_item_features( $tile->tile_item_id, $tile_depth + 1 );
     175        if ( $status != NOT_FOUND ) {
     176          return $status;
     177        }
     178      }
     179    }
     180    return NOT_FOUND;
     181  }
     182
     183  /**
     184   * Finds the width, height, bit depth and number of channels of the primary item.
     185   *
     186   * @return Status FOUND on success or NOT_FOUND on failure.
     187   */
     188  public function get_primary_item_features() {
     189    // Nothing to do without the primary item ID.
     190    if ( !$this->has_primary_item ) {
     191      return NOT_FOUND;
     192    }
     193    // Early exit.
     194    if ( empty( $this->dim_props ) || empty( $this->chan_props ) ) {
     195      return NOT_FOUND;
     196    }
     197    $status = $this->get_item_features( $this->primary_item_id, /*tile_depth=*/ 0 );
     198    if ( $status != FOUND ) {
     199      return $status;
     200    }
     201
     202    // "auxC" is parsed before the "ipma" properties so it is known now, if any.
     203    if ( $this->has_alpha ) {
     204      ++$this->primary_item_features['num_channels'];
     205    }
     206    return FOUND;
     207  }
     208}
     209
     210//------------------------------------------------------------------------------
     211
     212class Box {
     213  public $size; // In bytes.
     214  public $type; // Four characters.
     215  public $version; // 0 or actual version if this is a full box.
     216  public $flags; // 0 or actual value if this is a full box.
     217  public $content_size; // 'size' minus the header size.
     218
     219  /**
     220   * Reads the box header.
     221   *
     222   * @param stream  $handle              The resource the header will be parsed from.
     223   * @param int     $num_parsed_boxes    The total number of parsed boxes. Prevents timeouts.
     224   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     225   * @return Status                      FOUND on success or an error on failure.
     226   */
     227  public function parse( $handle, &$num_parsed_boxes, $num_remaining_bytes = MAX_SIZE ) {
     228    // See ISO/IEC 14496-12:2012(E) 4.2
     229    $header_size = 8; // box 32b size + 32b type (at least)
     230    if ( $header_size > $num_remaining_bytes ) {
     231      return INVALID;
     232    }
     233    if ( !( $data = read( $handle, 8 ) ) ) {
     234      return TRUNCATED;
     235    }
     236    $this->size = read_big_endian( $data, 4 );
     237    $this->type = substr( $data, 4, 4 );
     238    // 'box->size==1' means 64-bit size should be read after the box type.
     239    // 'box->size==0' means this box extends to all remaining bytes.
     240    if ( $this->size == 1 ) {
     241      $header_size += 8;
     242      if ( $header_size > $num_remaining_bytes ) {
     243        return INVALID;
     244      }
     245      if ( !( $data = read( $handle, 8 ) ) ) {
     246        return TRUNCATED;
     247      }
     248      // Stop the parsing if any box has a size greater than 4GB.
     249      if ( read_big_endian( $data, 4 ) != 0 ) {
     250        return ABORTED;
     251      }
     252      // Read the 32 least-significant bits.
     253      $this->size = read_big_endian( substr( $data, 4, 4 ), 4 );
     254    } else if ( $this->size == 0 ) {
     255      $this->size = $num_remaining_bytes;
     256    }
     257    if ( $this->size < $header_size ) {
     258      return INVALID;
     259    }
     260    if ( $this->size > $num_remaining_bytes ) {
     261      return INVALID;
     262    }
     263
     264    $has_fullbox_header = $this->type == 'meta' || $this->type == 'pitm' ||
     265                          $this->type == 'ipma' || $this->type == 'ispe' ||
     266                          $this->type == 'pixi' || $this->type == 'iref' ||
     267                          $this->type == 'auxC';
     268    if ( $has_fullbox_header ) {
     269      $header_size += 4;
     270    }
     271    if ( $this->size < $header_size ) {
     272      return INVALID;
     273    }
     274    $this->content_size = $this->size - $header_size;
     275    // Avoid timeouts. The maximum number of parsed boxes is arbitrary.
     276    ++$num_parsed_boxes;
     277    if ( $num_parsed_boxes >= MAX_NUM_BOXES ) {
     278      return ABORTED;
     279    }
     280
     281    $this->version = 0;
     282    $this->flags   = 0;
     283    if ( $has_fullbox_header ) {
     284      if ( !( $data = read( $handle, 4 ) ) ) {
     285        return TRUNCATED;
     286      }
     287      $this->version = read_big_endian( $data, 1 );
     288      $this->flags   = read_big_endian( substr( $data, 1, 3 ), 3 );
     289      // See AV1 Image File Format (AVIF) 8.1
     290      // at https://aomediacodec.github.io/av1-avif/#avif-boxes (available when
     291      // https://github.com/AOMediaCodec/av1-avif/pull/170 is merged).
     292      $is_parsable = ( $this->type == 'meta' && $this->version <= 0 ) ||
     293                     ( $this->type == 'pitm' && $this->version <= 1 ) ||
     294                     ( $this->type == 'ipma' && $this->version <= 1 ) ||
     295                     ( $this->type == 'ispe' && $this->version <= 0 ) ||
     296                     ( $this->type == 'pixi' && $this->version <= 0 ) ||
     297                     ( $this->type == 'iref' && $this->version <= 1 ) ||
     298                     ( $this->type == 'auxC' && $this->version <= 0 );
     299      // Instead of considering this file as invalid, skip unparsable boxes.
     300      if ( !$is_parsable ) {
     301        $this->type = 'unknownversion';
     302      }
     303    }
     304    // print_r( $this ); // Uncomment to print all boxes.
     305    return FOUND;
     306  }
     307}
     308
     309//------------------------------------------------------------------------------
     310
     311class Parser {
     312  private $handle; // Input stream.
     313  private $num_parsed_boxes = 0;
     314  private $data_was_skipped = false;
     315  public $features;
     316
     317  function __construct( $handle ) {
     318    $this->handle   = $handle;
     319    $this->features = new Features();
     320  }
     321
     322  /**
     323   * Parses an "ipco" box.
     324   *
     325   * "ispe" is used for width and height, "pixi" and "av1C" are used for bit depth
     326   * and number of channels, and "auxC" is used for alpha.
     327   *
     328   * @param stream  $handle              The resource the box will be parsed from.
     329   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     330   * @return Status                      FOUND on success or an error on failure.
     331   */
     332  private function parse_ipco( $num_remaining_bytes ) {
     333    $box_index = 1; // 1-based index. Used for iterating over properties.
     334    do {
     335      $box    = new Box();
     336      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     337      if ( $status != FOUND ) {
     338        return $status;
     339      }
     340
     341      if ( $box->type == 'ispe' ) {
     342        // See ISO/IEC 23008-12:2017(E) 6.5.3.2
     343        if ( $box->content_size < 8 ) {
     344          return INVALID;
     345        }
     346        if ( !( $data = read( $this->handle, 8 ) ) ) {
     347          return TRUNCATED;
     348        }
     349        $width  = read_big_endian( substr( $data, 0, 4 ), 4 );
     350        $height = read_big_endian( substr( $data, 4, 4 ), 4 );
     351        if ( $width == 0 || $height == 0 ) {
     352          return INVALID;
     353        }
     354        if ( count( $this->features->dim_props ) <= MAX_FEATURES &&
     355             $box_index <= MAX_VALUE ) {
     356          $dim_prop_count = count( $this->features->dim_props );
     357          $this->features->dim_props[$dim_prop_count]                 = new Dim_Prop();
     358          $this->features->dim_props[$dim_prop_count]->property_index = $box_index;
     359          $this->features->dim_props[$dim_prop_count]->width          = $width;
     360          $this->features->dim_props[$dim_prop_count]->height         = $height;
     361        } else {
     362          $this->data_was_skipped = true;
     363        }
     364        if ( !skip( $this->handle, $box->content_size - 8 ) ) {
     365          return TRUNCATED;
     366        }
     367      } else if ( $box->type == 'pixi' ) {
     368        // See ISO/IEC 23008-12:2017(E) 6.5.6.2
     369        if ( $box->content_size < 1 ) {
     370          return INVALID;
     371        }
     372        if ( !( $data = read( $this->handle, 1 ) ) ) {
     373          return TRUNCATED;
     374        }
     375        $num_channels = read_big_endian( $data, 1 );
     376        if ( $num_channels < 1 ) {
     377          return INVALID;
     378        }
     379        if ( $box->content_size < 1 + $num_channels ) {
     380          return INVALID;
     381        }
     382        if ( !( $data = read( $this->handle, 1 ) ) ) {
     383          return TRUNCATED;
     384        }
     385        $bit_depth = read_big_endian( $data, 1 );
     386        if ( $bit_depth < 1 ) {
     387          return INVALID;
     388        }
     389        for ( $i = 1; $i < $num_channels; ++$i ) {
     390          if ( !( $data = read( $this->handle, 1 ) ) ) {
     391            return TRUNCATED;
     392          }
     393          // Bit depth should be the same for all channels.
     394          if ( read_big_endian( $data, 1 ) != $bit_depth ) {
     395            return INVALID;
     396          }
     397          if ( $i > 32 ) {
     398            return ABORTED; // Be reasonable.
     399          }
     400        }
     401        if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
     402             $box_index <= MAX_VALUE && $bit_depth <= MAX_VALUE &&
     403             $num_channels <= MAX_VALUE ) {
     404          $chan_prop_count = count( $this->features->chan_props );
     405          $this->features->chan_props[$chan_prop_count]                 = new Chan_Prop();
     406          $this->features->chan_props[$chan_prop_count]->property_index = $box_index;
     407          $this->features->chan_props[$chan_prop_count]->bit_depth      = $bit_depth;
     408          $this->features->chan_props[$chan_prop_count]->num_channels   = $num_channels;
     409        } else {
     410          $this->data_was_skipped = true;
     411        }
     412        if ( !skip( $this->handle, $box->content_size - ( 1 + $num_channels ) ) ) {
     413          return TRUNCATED;
     414        }
     415      } else if ( $box->type == 'av1C' ) {
     416        // See AV1 Codec ISO Media File Format Binding 2.3.1
     417        // at https://aomediacodec.github.io/av1-isobmff/#av1c
     418        // Only parse the necessary third byte. Assume that the others are valid.
     419        if ( $box->content_size < 3 ) {
     420          return INVALID;
     421        }
     422        if ( !( $data = read( $this->handle, 3 ) ) ) {
     423          return TRUNCATED;
     424        }
     425        $byte          = read_big_endian( substr( $data, 2, 1 ), 1 );
     426        $high_bitdepth = ( $byte & 0x40 ) != 0;
     427        $twelve_bit    = ( $byte & 0x20 ) != 0;
     428        $monochrome    = ( $byte & 0x10 ) != 0;
     429        if ( $twelve_bit && !$high_bitdepth ) {
     430            return INVALID;
     431        }
     432        if ( count( $this->features->chan_props ) <= MAX_FEATURES &&
     433             $box_index <= MAX_VALUE ) {
     434          $chan_prop_count = count( $this->features->chan_props );
     435          $this->features->chan_props[$chan_prop_count]                 = new Chan_Prop();
     436          $this->features->chan_props[$chan_prop_count]->property_index = $box_index;
     437          $this->features->chan_props[$chan_prop_count]->bit_depth      =
     438              $high_bitdepth ? $twelve_bit ? 12 : 10 : 8;
     439          $this->features->chan_props[$chan_prop_count]->num_channels   = $monochrome ? 1 : 3;
     440        } else {
     441          $this->data_was_skipped = true;
     442        }
     443        if ( !skip( $this->handle, $box->content_size - 3 ) ) {
     444          return TRUNCATED;
     445        }
     446      } else if ( $box->type == 'auxC' ) {
     447        // See AV1 Image File Format (AVIF) 4
     448        // at https://aomediacodec.github.io/av1-avif/#auxiliary-images
     449        $kAlphaStr       = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha";
     450        $kAlphaStrLength = 44; // Includes terminating character.
     451        if ( $box->content_size >= $kAlphaStrLength ) {
     452          if ( !( $data = read( $this->handle, $kAlphaStrLength ) ) ) {
     453            return TRUNCATED;
     454          }
     455          if ( substr( $data, 0, $kAlphaStrLength ) == $kAlphaStr ) {
     456            // Note: It is unlikely but it is possible that this alpha plane does
     457            //       not belong to the primary item or a tile. Ignore this issue.
     458            $this->features->has_alpha = true;
     459          }
     460          if ( !skip( $this->handle, $box->content_size - $kAlphaStrLength ) ) {
     461            return TRUNCATED;
     462          }
     463        } else {
     464          if ( !skip( $this->handle, $box->content_size ) ) {
     465            return TRUNCATED;
     466          }
     467        }
     468      } else {
     469        if ( !skip( $this->handle, $box->content_size ) ) {
     470          return TRUNCATED;
     471        }
     472      }
     473      ++$box_index;
     474      $num_remaining_bytes -= $box->size;
     475    } while ( $num_remaining_bytes > 0 );
     476    return NOT_FOUND;
     477  }
     478
     479  /**
     480   * Parses an "iprp" box.
     481   *
     482   * The "ipco" box contain the properties which are linked to items by the "ipma" box.
     483   *
     484   * @param stream  $handle              The resource the box will be parsed from.
     485   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     486   * @return Status                      FOUND on success or an error on failure.
     487   */
     488  private function parse_iprp( $num_remaining_bytes ) {
     489    do {
     490      $box    = new Box();
     491      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     492      if ( $status != FOUND ) {
     493        return $status;
     494      }
     495
     496      if ( $box->type == 'ipco' ) {
     497        $status = $this->parse_ipco( $box->content_size );
     498        if ( $status != NOT_FOUND ) {
     499          return $status;
     500        }
     501      } else if ( $box->type == 'ipma' ) {
     502        // See ISO/IEC 23008-12:2017(E) 9.3.2
     503        $num_read_bytes = 4;
     504        if ( $box->content_size < $num_read_bytes ) {
     505          return INVALID;
     506        }
     507        if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
     508          return TRUNCATED;
     509        }
     510        $entry_count        = read_big_endian( $data, 4 );
     511        $id_num_bytes       = ( $box->version < 1 ) ? 2 : 4;
     512        $index_num_bytes    = ( $box->flags & 1 ) ? 2 : 1;
     513        $essential_bit_mask = ( $box->flags & 1 ) ? 0x8000 : 0x80;
     514
     515        for ( $entry = 0; $entry < $entry_count; ++$entry ) {
     516          if ( $entry >= MAX_PROPS ||
     517               count( $this->features->props ) >= MAX_PROPS ) {
     518            $this->data_was_skipped = true;
     519            break;
     520          }
     521          $num_read_bytes += $id_num_bytes + 1;
     522          if ( $box->content_size < $num_read_bytes ) {
     523            return INVALID;
     524          }
     525          if ( !( $data = read( $this->handle, $id_num_bytes + 1 ) ) ) {
     526            return TRUNCATED;
     527          }
     528          $item_id           = read_big_endian(
     529              substr( $data, 0, $id_num_bytes ), $id_num_bytes );
     530          $association_count = read_big_endian(
     531              substr( $data, $id_num_bytes, 1 ), 1 );
     532
     533          for ( $property = 0; $property < $association_count; ++$property ) {
     534            if ( $property >= MAX_PROPS ||
     535                 count( $this->features->props ) >= MAX_PROPS ) {
     536              $this->data_was_skipped = true;
     537              break;
     538            }
     539            $num_read_bytes += $index_num_bytes;
     540            if ( $box->content_size < $num_read_bytes ) {
     541              return INVALID;
     542            }
     543            if ( !( $data = read( $this->handle, $index_num_bytes ) ) ) {
     544              return TRUNCATED;
     545            }
     546            $value          = read_big_endian( $data, $index_num_bytes );
     547            // $essential = ($value & $essential_bit_mask);  // Unused.
     548            $property_index = ( $value & ~$essential_bit_mask );
     549            if ( $property_index <= MAX_VALUE && $item_id <= MAX_VALUE ) {
     550              $prop_count = count( $this->features->props );
     551              $this->features->props[$prop_count]                 = new Prop();
     552              $this->features->props[$prop_count]->property_index = $property_index;
     553              $this->features->props[$prop_count]->item_id        = $item_id;
     554            } else {
     555              $this->data_was_skipped = true;
     556            }
     557          }
     558          if ( $property < $association_count ) {
     559            break; // Do not read garbage.
     560          }
     561        }
     562
     563        // If all features are available now, do not look further.
     564        $status = $this->features->get_primary_item_features();
     565        if ( $status != NOT_FOUND ) {
     566          return $status;
     567        }
     568
     569        // Mostly if 'data_was_skipped'.
     570        if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
     571          return TRUNCATED;
     572        }
     573      } else {
     574        if ( !skip( $this->handle, $box->content_size ) ) {
     575          return TRUNCATED;
     576        }
     577      }
     578      $num_remaining_bytes -= $box->size;
     579    } while ( $num_remaining_bytes > 0 );
     580    return NOT_FOUND;
     581  }
     582
     583  /**
     584   * Parses an "iref" box.
     585   *
     586   * The "dimg" boxes contain links between tiles and their parent items, which
     587   * can be used to infer bit depth and number of channels for the primary item
     588   * when the latter does not have these properties.
     589   *
     590   * @param stream  $handle              The resource the box will be parsed from.
     591   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     592   * @return Status                      FOUND on success or an error on failure.
     593   */
     594  private function parse_iref( $num_remaining_bytes ) {
     595    do {
     596      $box    = new Box();
     597      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     598      if ( $status != FOUND ) {
     599        return $status;
     600      }
     601
     602      if ( $box->type == 'dimg' ) {
     603        // See ISO/IEC 14496-12:2015(E) 8.11.12.2
     604        $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
     605        $num_read_bytes   = $num_bytes_per_id + 2;
     606        if ( $box->content_size < $num_read_bytes ) {
     607          return INVALID;
     608        }
     609        if ( !( $data = read( $this->handle, $num_read_bytes ) ) ) {
     610          return TRUNCATED;
     611        }
     612        $from_item_id    = read_big_endian( $data, $num_bytes_per_id );
     613        $reference_count = read_big_endian( substr( $data, $num_bytes_per_id, 2 ), 2 );
     614
     615        for ( $i = 0; $i < $reference_count; ++$i ) {
     616          if ( $i >= MAX_TILES ) {
     617            $this->data_was_skipped = true;
     618            break;
     619          }
     620          $num_read_bytes += $num_bytes_per_id;
     621          if ( $box->content_size < $num_read_bytes ) {
     622            return INVALID;
     623          }
     624          if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
     625            return TRUNCATED;
     626          }
     627          $to_item_id = read_big_endian( $data, $num_bytes_per_id );
     628          $tile_count = count( $this->features->tiles );
     629          if ( $from_item_id <= MAX_VALUE && $to_item_id <= MAX_VALUE &&
     630               $tile_count < MAX_TILES ) {
     631            $this->features->tiles[$tile_count]                 = new Tile();
     632            $this->features->tiles[$tile_count]->tile_item_id   = $to_item_id;
     633            $this->features->tiles[$tile_count]->parent_item_id = $from_item_id;
     634          } else {
     635            $this->data_was_skipped = true;
     636          }
     637        }
     638
     639        // If all features are available now, do not look further.
     640        $status = $this->features->get_primary_item_features();
     641        if ( $status != NOT_FOUND ) {
     642          return $status;
     643        }
     644
     645        // Mostly if 'data_was_skipped'.
     646        if ( !skip( $this->handle, $box->content_size - $num_read_bytes ) ) {
     647          return TRUNCATED;
     648        }
     649      } else {
     650        if ( !skip( $this->handle, $box->content_size ) ) {
     651          return TRUNCATED;
     652        }
     653      }
     654      $num_remaining_bytes -= $box->size;
     655    } while ( $num_remaining_bytes > 0 );
     656    return NOT_FOUND;
     657  }
     658
     659  /**
     660   * Parses a "meta" box.
     661   *
     662   * It looks for the primary item ID in the "pitm" box and recurses into other boxes
     663   * to find its features.
     664   *
     665   * @param stream  $handle              The resource the box will be parsed from.
     666   * @param int     $num_remaining_bytes The number of bytes that should be available from the resource.
     667   * @return Status                      FOUND on success or an error on failure.
     668   */
     669  private function parse_meta( $num_remaining_bytes ) {
     670    do {
     671      $box    = new Box();
     672      $status = $box->parse( $this->handle, $this->num_parsed_boxes, $num_remaining_bytes );
     673      if ( $status != FOUND ) {
     674        return $status;
     675      }
     676
     677      if ( $box->type == 'pitm' ) {
     678        // See ISO/IEC 14496-12:2015(E) 8.11.4.2
     679        $num_bytes_per_id = ( $box->version == 0 ) ? 2 : 4;
     680        if ( $num_bytes_per_id > $num_remaining_bytes ) {
     681          return INVALID;
     682        }
     683        if ( !( $data = read( $this->handle, $num_bytes_per_id ) ) ) {
     684          return TRUNCATED;
     685        }
     686        $primary_item_id = read_big_endian( $data, $num_bytes_per_id );
     687        if ( $primary_item_id > MAX_VALUE ) {
     688          return ABORTED;
     689        }
     690        $this->features->has_primary_item = true;
     691        $this->features->primary_item_id  = $primary_item_id;
     692        if ( !skip( $this->handle, $box->content_size - $num_bytes_per_id ) ) {
     693          return TRUNCATED;
     694        }
     695      } else if ( $box->type == 'iprp' ) {
     696        $status = $this->parse_iprp( $box->content_size );
     697        if ( $status != NOT_FOUND ) {
     698          return $status;
     699        }
     700      } else if ( $box->type == 'iref' ) {
     701        $status = $this->parse_iref( $box->content_size );
     702        if ( $status != NOT_FOUND ) {
     703          return $status;
     704        }
     705      } else {
     706        if ( !skip( $this->handle, $box->content_size ) ) {
     707          return TRUNCATED;
     708        }
     709      }
     710      $num_remaining_bytes -= $box->size;
     711    } while ( $num_remaining_bytes != 0 );
     712    // According to ISO/IEC 14496-12:2012(E) 8.11.1.1 there is at most one "meta".
     713    return INVALID;
     714  }
     715
     716  /**
     717   * Parses a file stream.
     718   *
     719   * The file type is checked through the "ftyp" box.
     720   *
     721   * @return bool True if the input stream is an AVIF bitstream or false.
     722   */
     723  public function parse_ftyp() {
     724    $box    = new Box();
     725    $status = $box->parse( $this->handle, $this->num_parsed_boxes );
     726    if ( $status != FOUND ) {
     727      return false;
     728    }
     729
     730    if ( $box->type != 'ftyp' ) {
     731      return false;
     732    }
     733    // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1
     734    if ( $box->content_size < 8 ) {
     735      return false;
     736    }
     737    for ( $i = 0; $i + 4 <= $box->content_size; $i += 4 ) {
     738      if ( !( $data = read( $this->handle, 4 ) ) ) {
     739        return false;
     740      }
     741      if ( $i == 4 ) {
     742        continue; // Skip minor_version.
     743      }
     744      if ( substr( $data, 0, 4 ) == 'avif' || substr( $data, 0, 4 ) == 'avis' ) {
     745        return skip( $this->handle, $box->content_size - ( $i + 4 ) );
     746      }
     747      if ( $i > 32 * 4 ) {
     748        return false; // Be reasonable.
     749      }
     750
     751    }
     752    return false; // No AVIF brand no good.
     753  }
     754
     755  /**
     756   * Parses a file stream.
     757   *
     758   * Features are extracted from the "meta" box.
     759   *
     760   * @return bool True if the main features of the primary item were parsed or false.
     761   */
     762  public function parse_file() {
     763    $box = new Box();
     764    while ( $box->parse( $this->handle, $this->num_parsed_boxes ) == FOUND ) {
     765      if ( $box->type === 'meta' ) {
     766        if ( $this->parse_meta( $box->content_size ) != FOUND ) {
     767          return false;
     768        }
     769        return true;
     770      }
     771      if ( !skip( $this->handle, $box->content_size ) ) {
     772        return false;
     773      }
     774    }
     775    return false; // No "meta" no good.
     776  }
     777}
  • 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..02e3d204d7 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                // If we still don't have the image size, fall back to `wp_getimagesize`. This ensures AVIF images
     261                // are properly sized without affecting previous `getImageGeometry` behavior.
     262                if ( ( ! $width || ! $height ) && 'image/avif' === $this->mime_type ) {
     263                        $size   = wp_getimagesize( $this->file );
     264                        $width  = $size[0];
     265                        $height = $size[1];
     266                }
     267
    259268                return parent::update_size( $width, $height );
    260269        }
    261270
  • 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 5bfdbc23d6..3c8e911a3d 100644
    a b if ( ! defined( 'IMAGETYPE_WEBP' ) ) { 
    497497if ( ! defined( 'IMG_WEBP' ) ) {
    498498        define( 'IMG_WEBP', IMAGETYPE_WEBP );
    499499}
     500
     501// IMAGETYPE_AVIF constant is only defined in PHP 8.x or later.
     502if ( ! defined( 'IMAGETYPE_AVIF' ) ) {
     503        define( 'IMAGETYPE_AVIF', 19 );
     504}
     505
     506// IMG_AVIF constant is only defined in PHP 8.x or later.
     507if ( ! defined( 'IMG_AVIF' ) ) {
     508        define( 'IMG_AVIF', IMAGETYPE_AVIF );
     509}
  • 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 e63708f91b..b003c161d6 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 a3a5e56bfa..e5a39f588b 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                 * @todo check the third 4-byte token "[major_brand]", for "avif" or "avis" .
     3359                 *
     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..a2cb6ead84 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,
     5556        // extract the image size info from the file headers.
     5557        if ( 'image/avif' === wp_get_image_mime( $filename ) ) {
     5558                $avif_info = wp_get_avif_info( $filename );
     5559
     5560                $width     = $avif_info['width'];
     5561                $height    = $avif_info['height'];
     5562
     5563                // Mimic the native return format.
     5564                if ( $width && $height ) {
     5565                        return array(
     5566                                $width,
     5567                                $height,
     5568                                IMAGETYPE_AVIF,
     5569                                sprintf(
     5570                                        'width="%d" height="%d"',
     5571                                        $width,
     5572                                        $height
     5573                                ),
     5574                                'mime' => 'image/avif',
     5575                        );
     5576                }
     5577        }
     5578
    55445579        // The image could not be parsed.
    55455580        return false;
    55465581}
    55475582
     5583/**
     5584 * Extracts meta information about an AVIF file: width, height, bit depth, and number of channels.
     5585 *
     5586 * @since 6.5.0
     5587 *
     5588 * @param string $filename Path to an AVIF file.
     5589 * @return array {
     5590 *    An array of AVIF image information.
     5591 *
     5592 *   @type int|false    $width        Image width on success, false on failure.
     5593 *   @type int|false    $height       Image height on success, false on failure.
     5594 *   @type int|false    $bit_depth    Image bit depth on success, false on failure.
     5595 *   @type int|false    $num_channels Image number of channels on success, false on failure.
     5596 * }
     5597 */
     5598function wp_get_avif_info( $filename ) {
     5599        $width  = false;
     5600        $height = false;
     5601        $type   = false;
     5602
     5603        if ( 'image/avif' !== wp_get_image_mime( $filename ) ) {
     5604                return compact( 'width', 'height', 'type' );
     5605        }
     5606
     5607        // Parse the file using libavifinfo's PHP implementation.
     5608        require_once ABSPATH . WPINC . '/class-avif-info.php';
     5609        $results = array(
     5610                'width'        => false,
     5611                'height'       => false,
     5612                'bit_depth'    => false,
     5613                'num_channels' => false,
     5614        );
     5615
     5616        $handle = fopen( $filename, 'rb' );
     5617        if ( $handle ) {
     5618                $parser  = new Avifinfo\Parser( $handle );
     5619                $success = $parser->parse_ftyp() && $parser->parse_file();
     5620                fclose( $handle );
     5621                if ( $success ) {
     5622                        $results = $parser->features->primary_item_features;
     5623                }
     5624        }
     5625        return $results;
     5626}
     5627
    55485628/**
    55495629 * Extracts meta information about a WebP file: width, height, and type.
    55505630 *
  • 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 7367c0fc57..bccbfa559d 100644
    a b class WP_REST_Attachments_Controller extends WP_REST_Posts_Controller { 
    450450                        );
    451451                }
    452452
    453                 $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp' );
     453                $supported_types = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif' );
    454454                $mime_type       = get_post_mime_type( $attachment_id );
    455455                if ( ! in_array( $mime_type, $supported_types, true ) ) {
    456456                        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-rotated.avif b/tests/phpunit/data/images/avif-rotated.avif
    new file mode 100644
    index 0000000000..ee7c5246e1
    Binary files /dev/null and b/tests/phpunit/data/images/avif-rotated.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..db34cd3f74
    Binary files /dev/null and b/tests/phpunit/data/images/avif-transparent.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..1244e761cf 100644
    a b class Tests_Image_Editor extends WP_Image_UnitTestCase { 
    363363                        ),
    364364                );
    365365        }
     366
     367        /**
     368         * Test wp_get_avif_info.
     369         *
     370         * @ticket 51228
     371         * @dataProvider data_wp_get_avif_info
     372         *
     373         */
     374        public function test_wp_get_avif_info( $file, $expected ) {
     375                $editor = wp_get_image_editor( $file );
     376
     377                if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/avif' ) ) {
     378                        $this->markTestSkipped( sprintf( 'No AVIF support in the editor engine %s on this system.', $this->editor_engine ) );
     379                }
     380
     381                $file_data = wp_get_avif_info( $file );
     382                $this->assertSame( $expected, $file_data );
     383        }
     384
     385        /**
     386         * Data provider for test_wp_get_avif_info().
     387         */
     388        public function data_wp_get_avif_info() {
     389                return array(
     390                        // Standard JPEG.
     391                        array(
     392                                DIR_TESTDATA . '/images/test-image.jpg',
     393                                array(
     394                                        'width'  => false,
     395                                        'height' => false,
     396                                        'type'   => false,
     397                                ),
     398                        ),
     399                        // Standard GIF.
     400                        array(
     401                                DIR_TESTDATA . '/images/test-image.gif',
     402                                array(
     403                                        'width'  => false,
     404                                        'height' => false,
     405                                        'type'   => false,
     406                                ),
     407                        ),
     408                        // Animated AVIF.
     409                        array(
     410                                DIR_TESTDATA . '/images/avif-animated.avif',
     411                                array(
     412                                        'width'        => 150,
     413                                        'height'       => 150,
     414                                        'bit_depth'    => 8,
     415                                        'num_channels' => 3,
     416                                ),
     417                        ),
     418                        // Lossless AVIF.
     419                        array(
     420                                DIR_TESTDATA . '/images/avif-lossless.avif',
     421                                array(
     422                                        'width'        => 400,
     423                                        'height'       => 400,
     424                                        'bit_depth'    => 8,
     425                                        'num_channels' => 3,
     426                                ),
     427                        ),
     428                        // Lossy AVIF.
     429                        array(
     430                                DIR_TESTDATA . '/images/avif-lossy.avif',
     431                                array(
     432                                        'width'        => 400,
     433                                        'height'       => 400,
     434                                        'bit_depth'    => 8,
     435                                        'num_channels' => 3,
     436                                ),
     437                        ),
     438                        // Transparent AVIF.
     439                        array(
     440                                DIR_TESTDATA . '/images/avif-transparent.avif',
     441                                array(
     442                                        'width'        => 128,
     443                                        'height'       => 128,
     444                                        'bit_depth'    => 12,
     445                                        'num_channels' => 3,
     446                                ),
     447                        ),
     448                );
     449        }
    366450}
  • 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..094e1d341e 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
     117
    91118        public function test_resize_larger() {
    92119                // image_resize() should refuse to make an image larger.
    93120                $image = $this->resize_helper( DIR_TESTDATA . '/images/test-image.jpg', 100, 100 );