diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 44fd623fa5ad2..72963b4b741aa 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -89,6 +89,7 @@ static function ( $classes ) { '/?_fields=' . implode( ',', array( + 'animated_image_subsizes', 'description', 'gmt_offset', 'home', diff --git a/src/wp-admin/site-editor.php b/src/wp-admin/site-editor.php index 9a8268c3392d7..e6aca2e6fdf1a 100644 --- a/src/wp-admin/site-editor.php +++ b/src/wp-admin/site-editor.php @@ -215,6 +215,7 @@ static function ( $classes ) { '/?_fields=' . implode( ',', array( + 'animated_image_subsizes', 'description', 'gmt_offset', 'home', diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index af73b80103546..dc9364db769e6 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1378,6 +1378,27 @@ public function get_index( $request ) { /** This filter is documented in wp-admin/includes/image.php */ $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); + + /** + * Filters whether sub-sizes of animated images should keep their animation. + * + * By default, sub-sizes of animated images (e.g. animated GIFs) are static, + * generated from the first frame only. Re-encoding every frame per sub-size + * is very resource intensive, so animated sub-sizes are opt-in. + * + * This currently only affects the client-side media processing path, + * where all frames can be decoded and re-encoded in the browser. Only + * uncropped sub-sizes keep their animation; cropped sizes (such as + * `thumbnail`) are always generated from the first frame. Uploads that + * take the server-side path also still produce static sub-sizes, as + * neither GD nor Imagick resizing preserves animation in core. + * + * @since 7.2.0 + * + * @param bool $animated_image_subsizes Whether to generate animated sub-sizes + * for animated images. Default false. + */ + $available['animated_image_subsizes'] = (bool) apply_filters( 'wp_generate_animated_image_subsizes', false ); } $response = new WP_REST_Response( $available ); diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index f61d2fc745d99..081a0e8249981 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1207,6 +1207,64 @@ public function test_get_index() { $this->assertArrayHasKey( 'site_icon_url', $data ); } + /** + * @ticket 65656 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_should_include_animated_image_subsizes(): void { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $this->assertIsInt( $user_id ); + wp_set_current_user( $user_id ); + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + $this->assertIsArray( $data ); + + $this->assertArrayHasKey( 'animated_image_subsizes', $data ); + $this->assertFalse( $data['animated_image_subsizes'] ); + } + + /** + * @ticket 65656 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_should_not_include_animated_image_subsizes_without_caps(): void { + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + $this->assertIsArray( $data ); + + $this->assertArrayNotHasKey( 'animated_image_subsizes', $data ); + } + + /** + * @ticket 65656 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_should_honor_animated_image_subsizes_filter(): void { + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $this->assertIsInt( $user_id ); + wp_set_current_user( $user_id ); + add_filter( 'wp_client_side_media_processing_enabled', '__return_true' ); + add_filter( 'wp_generate_animated_image_subsizes', '__return_true' ); + + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + + $this->assertTrue( $data['animated_image_subsizes'] ); + } + /** * @ticket 57902 * diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 1be56017826ff..8e84da13246cc 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -13061,6 +13061,7 @@ mockedApiResponse.Schema = { } }, "image_size_threshold": 2560, + "animated_image_subsizes": false, "site_logo": 0, "site_icon": 0, "site_icon_url": ""