diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index a6d198ff15343..706bbc847b2ba 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -93,6 +93,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 4289f89f7102c..d39cbd722a80f 100644 --- a/src/wp-admin/site-editor.php +++ b/src/wp-admin/site-editor.php @@ -219,6 +219,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 c331d73363541..c25890d974072 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1398,6 +1398,27 @@ public function get_index( $request ) { */ /** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */ $available['image_max_bit_depth'] = (int) apply_filters( 'image_max_bit_depth', 16, 16 ); + + /** + * 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 b8e8cc6f5adcb..b83bceb971e5e 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1209,6 +1209,7 @@ public function test_get_index() { /** * @ticket 64804 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ @@ -1230,10 +1231,13 @@ public function test_get_index_should_include_media_processing_settings(): void $this->assertTrue( $data['image_strip_meta'] ); $this->assertArrayHasKey( 'image_max_bit_depth', $data ); $this->assertSame( 16, $data['image_max_bit_depth'] ); + $this->assertArrayHasKey( 'animated_image_subsizes', $data ); + $this->assertFalse( $data['animated_image_subsizes'] ); } /** * @ticket 64804 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ @@ -1250,10 +1254,12 @@ public function test_get_index_should_not_include_media_processing_settings_with $this->assertArrayNotHasKey( 'image_size_threshold', $data ); $this->assertArrayNotHasKey( 'image_strip_meta', $data ); $this->assertArrayNotHasKey( 'image_max_bit_depth', $data ); + $this->assertArrayNotHasKey( 'animated_image_subsizes', $data ); } /** * @ticket 64804 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ @@ -1267,6 +1273,7 @@ public function test_get_index_should_honor_media_processing_filters(): void { 'image_max_bit_depth', static fn ( int $max_depth ) => min( 8, $max_depth ) ); + add_filter( 'wp_generate_animated_image_subsizes', '__return_true' ); $server = new WP_REST_Server(); $request = new WP_REST_Request( 'GET', '/' ); @@ -1275,6 +1282,7 @@ public function test_get_index_should_honor_media_processing_filters(): void { $this->assertFalse( $data['image_strip_meta'] ); $this->assertSame( 8, $data['image_max_bit_depth'] ); + $this->assertTrue( $data['animated_image_subsizes'] ); } /** diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 1b0eaac6cccd1..4011d78a1617f 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -13063,6 +13063,7 @@ mockedApiResponse.Schema = { "image_size_threshold": 2560, "image_strip_meta": true, "image_max_bit_depth": 16, + "animated_image_subsizes": false, "site_logo": 0, "site_icon": 0, "site_icon_url": ""