From 42bcf794427919fe57f147540151b5c89e278123 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 16 Jul 2026 16:49:17 -0700 Subject: [PATCH 1/3] Media: Add the wp_generate_animated_image_subsizes filter to the REST index. Sub-sizes of animated images are static first-frame images by default because re-encoding every frame per sub-size is very resource intensive. The client-side media processing path can preserve animation in the browser, so expose an opt-in flag on the REST API index (alongside image_size_threshold) that the client pipeline honors for uncropped sub-sizes. See https://github.com/WordPress/gutenberg/issues/80383 and https://github.com/WordPress/gutenberg/pull/80385. --- src/wp-admin/edit-form-blocks.php | 1 + src/wp-admin/site-editor.php | 1 + .../rest-api/class-wp-rest-server.php | 21 +++++++ tests/phpunit/tests/rest-api/rest-server.php | 58 +++++++++++++++++++ tests/qunit/fixtures/wp-api-generated.js | 1 + 5 files changed, 82 insertions(+) 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..3246c023a67be 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 28474 + * + * @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 28474 + * + * @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 28474 + * + * @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": "" From 1476c8142954e0218a70872d2c7d2266ed63bad2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 16 Jul 2026 16:55:18 -0700 Subject: [PATCH 2/3] Reference ticket 65656 in the new REST index tests. --- tests/phpunit/tests/rest-api/rest-server.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 3246c023a67be..081a0e8249981 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1208,7 +1208,7 @@ public function test_get_index() { } /** - * @ticket 28474 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ @@ -1229,7 +1229,7 @@ public function test_get_index_should_include_animated_image_subsizes(): void { } /** - * @ticket 28474 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ @@ -1246,7 +1246,7 @@ public function test_get_index_should_not_include_animated_image_subsizes_withou } /** - * @ticket 28474 + * @ticket 65656 * * @covers WP_REST_Server::get_index */ From ce1610e8a063087ca872f1e42d46287eb1a69b2d Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 1 Oct 2026 09:41:52 -0700 Subject: [PATCH 3/3] Rename REST index field to generate_animated_image_subsizes A bare `animated_image_subsizes` reads like an array of sizes rather than a boolean. Prefix it with `generate_` so the field name mirrors the filter it exposes, and keep the preload field lists alphabetical so they still match the editor's entities.js list exactly. Claude-Session: https://claude.ai/code/session_018uRsBsDb9t3piAjyWaycCo --- src/wp-admin/edit-form-blocks.php | 2 +- src/wp-admin/site-editor.php | 2 +- src/wp-includes/rest-api/class-wp-rest-server.php | 6 +++--- tests/phpunit/tests/rest-api/rest-server.php | 8 ++++---- tests/qunit/fixtures/wp-api-generated.js | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-admin/edit-form-blocks.php b/src/wp-admin/edit-form-blocks.php index 706bbc847b2ba..0a613e3016bcd 100644 --- a/src/wp-admin/edit-form-blocks.php +++ b/src/wp-admin/edit-form-blocks.php @@ -93,8 +93,8 @@ static function ( $classes ) { '/?_fields=' . implode( ',', array( - 'animated_image_subsizes', 'description', + 'generate_animated_image_subsizes', 'gmt_offset', 'home', 'image_max_bit_depth', diff --git a/src/wp-admin/site-editor.php b/src/wp-admin/site-editor.php index d39cbd722a80f..076ad053d2971 100644 --- a/src/wp-admin/site-editor.php +++ b/src/wp-admin/site-editor.php @@ -219,8 +219,8 @@ static function ( $classes ) { '/?_fields=' . implode( ',', array( - 'animated_image_subsizes', 'description', + 'generate_animated_image_subsizes', 'gmt_offset', 'home', 'image_max_bit_depth', 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 b6a7877d24b28..f794e9d1592af 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1480,10 +1480,10 @@ public function get_index( $request ) { * * @since 7.2.0 * - * @param bool $animated_image_subsizes Whether to generate animated sub-sizes - * for animated images. Default false. + * @param bool $generate_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 ); + $available['generate_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 6f6a42cfa4b35..f081889951a76 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1234,8 +1234,8 @@ 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'] ); + $this->assertArrayHasKey( 'generate_animated_image_subsizes', $data ); + $this->assertFalse( $data['generate_animated_image_subsizes'] ); } /** @@ -1257,7 +1257,7 @@ 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 ); + $this->assertArrayNotHasKey( 'generate_animated_image_subsizes', $data ); } /** @@ -1284,7 +1284,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'] ); + $this->assertTrue( $data['generate_animated_image_subsizes'] ); } /** diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 8eefc79b812d7..93a838338bba4 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -13078,7 +13078,7 @@ mockedApiResponse.Schema = { "image_size_threshold": 2560, "image_strip_meta": true, "image_max_bit_depth": 16, - "animated_image_subsizes": false, + "generate_animated_image_subsizes": false, "site_logo": 0, "site_icon": 0, "site_icon_url": ""