From f84d32d7e272b291fa035d2612f0764646a84f69 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 14:06:02 -0400 Subject: [PATCH 1/8] REST API: Expose per-attachment output format and progressive flags on attachment responses. Add two readonly fields to `WP_REST_Attachments_Controller`, in the `edit` context, alongside the existing `exif_orientation` field: * `image_output_format` returns the resolved output MIME type when `image_editor_output_format` maps the source MIME to a different one (e.g. JPEG -> WebP), or `null` when no conversion is needed. The filter is invoked with the real attached filename and MIME type so plugins can make per-file decisions, mirroring the way `WP_Image_Editor::set_quality()` resolves the output format. * `image_save_progressive` returns the boolean result of the `image_save_progressive` filter for the attachment's MIME type. Both fields are evaluated only for image attachments. Client-side media processing now has the per-file context it needs to encode sub-sizes consistently with server-side behavior, instead of relying on the file-less generic values previously surfaced on the REST API root index. Adds focused PHPUnit coverage for the schema, the default response, custom filter behavior, and that the fields are skipped for non-image attachments. See #65367. Backport of Gutenberg PR #75793 (https://github.com/WordPress/gutenberg/pull/75793). --- .../class-wp-rest-attachments-controller.php | 43 +++++ .../rest-api/rest-attachments-controller.php | 158 +++++++++++++++++- 2 files changed, 200 insertions(+), 1 deletion(-) 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 21805778ba659..0594120a41c4d 100644 --- 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 @@ -1183,6 +1183,35 @@ public function prepare_item_for_response( $item, $request ) { $data['exif_orientation'] = $orientation; } + // Per-file output format for images, evaluated with the real filename + // and MIME type so plugins filtering image_editor_output_format can + // make per-attachment decisions (e.g. JPEG -> WebP). Resolved the same + // way WP_Image_Editor::set_quality() resolves the output format. + if ( in_array( 'image_output_format', $fields, true ) && wp_attachment_is_image( $post ) ) { + $mime_type = get_post_mime_type( $post ); + $filename = get_attached_file( $post->ID ); + + /** This filter is documented in wp-includes/class-wp-image-editor.php */ + $output_formats = apply_filters( + 'image_editor_output_format', + array( $mime_type => $mime_type ), + $filename ? $filename : '', + $mime_type + ); + + $output_mime = $output_formats[ $mime_type ] ?? $mime_type; + $data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null; + } + + // Per-file progressive/interlaced encoding flag for images, evaluated + // against the attachment's MIME type. + if ( in_array( 'image_save_progressive', $fields, true ) && wp_attachment_is_image( $post ) ) { + $mime_type = get_post_mime_type( $post ); + + /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ + $data['image_save_progressive'] = (bool) apply_filters( 'image_save_progressive', false, $mime_type ); + } + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->filter_response_by_context( $data, $context ); @@ -1373,6 +1402,20 @@ public function get_item_schema() { 'readonly' => true, ); + $schema['properties']['image_output_format'] = array( + 'description' => __( 'The output MIME type this image should be converted to, based on the image_editor_output_format filter. Null if no conversion is needed.' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['image_save_progressive'] = array( + 'description' => __( 'Whether to use progressive/interlaced encoding when saving this image.' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + unset( $schema['properties']['password'] ); $this->schema = $schema; diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 79e9d23cf9dd3..f39f018414da1 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1951,10 +1951,12 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 32, $properties ); + $this->assertCount( 34, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'alt_text', $properties ); $this->assertArrayHasKey( 'exif_orientation', $properties ); + $this->assertArrayHasKey( 'image_output_format', $properties ); + $this->assertArrayHasKey( 'image_save_progressive', $properties ); $this->assertArrayHasKey( 'filename', $properties ); $this->assertArrayHasKey( 'filesize', $properties ); $this->assertArrayHasKey( 'caption', $properties ); @@ -1992,6 +1994,160 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'class_list', $properties ); } + /** + * Tests the image_output_format / image_save_progressive schema properties. + * + * @ticket 65367 + * + * @covers WP_REST_Attachments_Controller::get_item_schema + */ + public function test_image_output_format_and_progressive_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertArrayHasKey( 'image_output_format', $properties ); + $this->assertSame( array( 'string', 'null' ), $properties['image_output_format']['type'] ); + $this->assertSame( array( 'edit' ), $properties['image_output_format']['context'] ); + $this->assertTrue( $properties['image_output_format']['readonly'] ); + + $this->assertArrayHasKey( 'image_save_progressive', $properties ); + $this->assertSame( 'boolean', $properties['image_save_progressive']['type'] ); + $this->assertSame( array( 'edit' ), $properties['image_save_progressive']['context'] ); + $this->assertTrue( $properties['image_save_progressive']['readonly'] ); + } + + /** + * Verifies image_output_format is null by default (no conversion needed) and + * image_save_progressive defaults to false on a freshly uploaded JPEG. + * + * @ticket 65367 + * + * @covers WP_REST_Attachments_Controller::create_item + * @covers WP_REST_Attachments_Controller::prepare_item_for_response + */ + public function test_image_output_format_and_progressive_defaults_in_create_response() { + wp_set_current_user( self::$superadmin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_param( 'context', 'edit' ); + $request->set_param( 'generate_sub_sizes', false ); + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/canola.jpg' ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertArrayHasKey( 'image_output_format', $data ); + $this->assertNull( $data['image_output_format'] ); + $this->assertArrayHasKey( 'image_save_progressive', $data ); + $this->assertFalse( $data['image_save_progressive'] ); + } + + /** + * Verifies image_output_format reflects an image_editor_output_format filter + * that remaps JPEG to WebP, and that the filter sees the real attached + * filename and MIME type. + * + * @ticket 65367 + * + * @covers WP_REST_Attachments_Controller::prepare_item_for_response + */ + public function test_image_output_format_with_custom_filter() { + wp_set_current_user( self::$superadmin_id ); + + $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $captured = array(); + $filter = static function ( $formats, $filename, $mime_type ) use ( &$captured ) { + $captured['filename'] = $filename; + $captured['mime_type'] = $mime_type; + $formats['image/jpeg'] = 'image/webp'; + return $formats; + }; + add_filter( 'image_editor_output_format', $filter, 10, 3 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + remove_filter( 'image_editor_output_format', $filter, 10 ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'image_output_format', $data ); + $this->assertSame( 'image/webp', $data['image_output_format'] ); + + // The filter must be invoked with the real attached filename and MIME type. + $this->assertStringEndsWith( '.jpg', (string) $captured['filename'] ); + $this->assertSame( 'image/jpeg', $captured['mime_type'] ); + } + + /** + * Verifies image_save_progressive surfaces the filter result for the + * attachment's MIME type. + * + * @ticket 65367 + * + * @covers WP_REST_Attachments_Controller::prepare_item_for_response + */ + public function test_image_save_progressive_with_custom_filter() { + wp_set_current_user( self::$superadmin_id ); + + $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $filter = static function ( $progressive, $mime_type ) { + return 'image/jpeg' === $mime_type; + }; + add_filter( 'image_save_progressive', $filter, 10, 2 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + remove_filter( 'image_save_progressive', $filter, 10 ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayHasKey( 'image_save_progressive', $data ); + $this->assertTrue( $data['image_save_progressive'] ); + } + + /** + * Non-image attachments must not surface the image_* fields. + * + * @ticket 65367 + * + * @covers WP_REST_Attachments_Controller::prepare_item_for_response + */ + public function test_image_output_format_skipped_for_non_image() { + wp_set_current_user( self::$superadmin_id ); + + $attachment_id = self::factory()->attachment->create_object( + DIR_TESTDATA . '/uploads/dashicons.woff', + 0, + array( + 'post_mime_type' => 'application/font-woff', + 'post_type' => 'attachment', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertArrayNotHasKey( 'image_output_format', $data ); + $this->assertArrayNotHasKey( 'image_save_progressive', $data ); + } + public function test_get_additional_field_registration() { $schema = array( From 4c7bb38fa06d4116d1fd4b042701a0116a45c563 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 15:21:08 -0400 Subject: [PATCH 2/8] REST API: Remove redundant client-side media output format settings from the index. The image_output_formats, jpeg_interlaced, png_interlaced, and gif_interlaced settings were exposed on the REST API root index without any per-file context, so the image_editor_output_format filter ran with an empty filename and could not make per-file decisions. These values are now provided per attachment via the image_output_format and image_save_progressive response fields added in the attachments controller, making the file-less index values redundant. Keep image_sizes and image_size_threshold on the index, which remain useful without file context. Update the generated REST API qunit fixture to match. --- .../rest-api/class-wp-rest-server.php | 16 ---------------- tests/qunit/fixtures/wp-api-generated.js | 4 ---- 2 files changed, 20 deletions(-) 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 704a990298826..ec66f4a70a525 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -1378,22 +1378,6 @@ 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 ); - - // Image output formats. - $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' ); - $output_formats = array(); - foreach ( $input_formats as $mime_type ) { - /** This filter is documented in wp-includes/media.php */ - $output_formats = apply_filters( 'image_editor_output_format', $output_formats, '', $mime_type ); - } - $available['image_output_formats'] = (object) $output_formats; - - /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ - $available['jpeg_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/jpeg' ); - /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ - $available['png_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/png' ); - /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ - $available['gif_interlaced'] = (bool) apply_filters( 'image_save_progressive', false, 'image/gif' ); } $response = new WP_REST_Response( $available ); diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index fa03d9751fe99..1a2d728a2d7cd 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -12806,10 +12806,6 @@ mockedApiResponse.Schema = { } }, "image_size_threshold": 2560, - "image_output_formats": {}, - "jpeg_interlaced": false, - "png_interlaced": false, - "gif_interlaced": false, "site_logo": 0, "site_icon": 0, "site_icon_url": "" From 3c52730f3297a44e70cb9bf57bea2438bfd3a99a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 May 2026 09:57:41 -0700 Subject: [PATCH 3/8] Add void return type hints --- .../tests/rest-api/rest-attachments-controller.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index f39f018414da1..493c1858585c4 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2001,7 +2001,7 @@ public function test_get_item_schema() { * * @covers WP_REST_Attachments_Controller::get_item_schema */ - public function test_image_output_format_and_progressive_schema() { + public function test_image_output_format_and_progressive_schema(): void { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -2027,7 +2027,7 @@ public function test_image_output_format_and_progressive_schema() { * @covers WP_REST_Attachments_Controller::create_item * @covers WP_REST_Attachments_Controller::prepare_item_for_response */ - public function test_image_output_format_and_progressive_defaults_in_create_response() { + public function test_image_output_format_and_progressive_defaults_in_create_response(): void { wp_set_current_user( self::$superadmin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); @@ -2056,7 +2056,7 @@ public function test_image_output_format_and_progressive_defaults_in_create_resp * * @covers WP_REST_Attachments_Controller::prepare_item_for_response */ - public function test_image_output_format_with_custom_filter() { + public function test_image_output_format_with_custom_filter(): void { wp_set_current_user( self::$superadmin_id ); $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); @@ -2095,7 +2095,7 @@ public function test_image_output_format_with_custom_filter() { * * @covers WP_REST_Attachments_Controller::prepare_item_for_response */ - public function test_image_save_progressive_with_custom_filter() { + public function test_image_save_progressive_with_custom_filter(): void { wp_set_current_user( self::$superadmin_id ); $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); @@ -2125,7 +2125,7 @@ public function test_image_save_progressive_with_custom_filter() { * * @covers WP_REST_Attachments_Controller::prepare_item_for_response */ - public function test_image_output_format_skipped_for_non_image() { + public function test_image_output_format_skipped_for_non_image(): void { wp_set_current_user( self::$superadmin_id ); $attachment_id = self::factory()->attachment->create_object( From 4001bc19b4216c97d990a934a77540057b8c873b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 May 2026 10:13:01 -0700 Subject: [PATCH 4/8] Add array shape to return type for WP_REST_Posts_Controller::get_item_schema() --- .../rest-api/endpoints/class-wp-rest-posts-controller.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 0ab54a3a0d384..83c83ad6d5720 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -2404,6 +2404,13 @@ protected function get_available_actions( $post, $request ) { * @since 4.7.0 * * @return array Item schema data. + * + * @phpstan-return array{ + * title: non-empty-string, + * type: non-empty-string, + * properties: array>, + * ... + * } */ public function get_item_schema() { if ( $this->schema ) { From 147b8740e0b414a0066cb51de12da914afec916c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 May 2026 10:19:33 -0700 Subject: [PATCH 5/8] Declare type for data in response --- .../endpoints/class-wp-rest-attachments-controller.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 0594120a41c4d..dbc9380f2c9d6 100644 --- 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 @@ -1042,7 +1042,8 @@ public function prepare_item_for_response( $item, $request ) { $response = parent::prepare_item_for_response( $post, $request ); $fields = $this->get_fields_for_response( $request ); - $data = $response->get_data(); + /** @var array $data */ + $data = $response->get_data(); if ( in_array( 'description', $fields, true ) ) { $data['description'] = array( @@ -1191,7 +1192,7 @@ public function prepare_item_for_response( $item, $request ) { $mime_type = get_post_mime_type( $post ); $filename = get_attached_file( $post->ID ); - /** This filter is documented in wp-includes/class-wp-image-editor.php */ + /** This filter is documented in wp-includes/media.php */ $output_formats = apply_filters( 'image_editor_output_format', array( $mime_type => $mime_type ), From a3d01e44f8b854c7b8d689122994d6b809fe665a Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 29 May 2026 14:16:22 -0400 Subject: [PATCH 6/8] Update tests/phpunit/tests/rest-api/rest-attachments-controller.php Co-authored-by: Weston Ruter --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 493c1858585c4..723a4a18d37e3 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2076,8 +2076,6 @@ public function test_image_output_format_with_custom_filter(): void { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - remove_filter( 'image_editor_output_format', $filter, 10 ); - $this->assertSame( 200, $response->get_status() ); $this->assertArrayHasKey( 'image_output_format', $data ); $this->assertSame( 'image/webp', $data['image_output_format'] ); From 4607a4a067744f20ecb7be7dc60ec37de1083f0b Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Fri, 29 May 2026 14:16:36 -0400 Subject: [PATCH 7/8] Update tests/phpunit/tests/rest-api/rest-attachments-controller.php Co-authored-by: Weston Ruter --- tests/phpunit/tests/rest-api/rest-attachments-controller.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 723a4a18d37e3..4951bc1341275 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2109,8 +2109,6 @@ public function test_image_save_progressive_with_custom_filter(): void { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - remove_filter( 'image_save_progressive', $filter, 10 ); - $this->assertSame( 200, $response->get_status() ); $this->assertArrayHasKey( 'image_save_progressive', $data ); $this->assertTrue( $data['image_save_progressive'] ); From 823f40a90bbd0f9907a54a40e81f8ba3f0311103 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 29 May 2026 14:25:30 -0400 Subject: [PATCH 8/8] REST API: Address review feedback on per-attachment image fields. Factor the shared `wp_attachment_is_image()` guard out of the `image_output_format` and `image_save_progressive` branches so the attachment MIME type is resolved once and reused by both. Inline the filter closures in the corresponding tests now that the manual `remove_filter()` calls are gone, dropping the unneeded `$filter` variables. --- .../class-wp-rest-attachments-controller.php | 47 ++++++++++--------- .../rest-api/rest-attachments-controller.php | 30 +++++++----- 2 files changed, 43 insertions(+), 34 deletions(-) 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 dbc9380f2c9d6..4e99c5931dfb0 100644 --- 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 @@ -1184,33 +1184,34 @@ public function prepare_item_for_response( $item, $request ) { $data['exif_orientation'] = $orientation; } - // Per-file output format for images, evaluated with the real filename - // and MIME type so plugins filtering image_editor_output_format can - // make per-attachment decisions (e.g. JPEG -> WebP). Resolved the same - // way WP_Image_Editor::set_quality() resolves the output format. - if ( in_array( 'image_output_format', $fields, true ) && wp_attachment_is_image( $post ) ) { + if ( wp_attachment_is_image( $post ) ) { $mime_type = get_post_mime_type( $post ); - $filename = get_attached_file( $post->ID ); - - /** This filter is documented in wp-includes/media.php */ - $output_formats = apply_filters( - 'image_editor_output_format', - array( $mime_type => $mime_type ), - $filename ? $filename : '', - $mime_type - ); - $output_mime = $output_formats[ $mime_type ] ?? $mime_type; - $data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null; - } + // Per-file output format for images, evaluated with the real filename + // and MIME type so plugins filtering image_editor_output_format can + // make per-attachment decisions (e.g. JPEG -> WebP). Resolved the same + // way WP_Image_Editor::set_quality() resolves the output format. + if ( in_array( 'image_output_format', $fields, true ) ) { + $filename = get_attached_file( $post->ID ); + + /** This filter is documented in wp-includes/media.php */ + $output_formats = apply_filters( + 'image_editor_output_format', + array( $mime_type => $mime_type ), + $filename ? $filename : '', + $mime_type + ); - // Per-file progressive/interlaced encoding flag for images, evaluated - // against the attachment's MIME type. - if ( in_array( 'image_save_progressive', $fields, true ) && wp_attachment_is_image( $post ) ) { - $mime_type = get_post_mime_type( $post ); + $output_mime = $output_formats[ $mime_type ] ?? $mime_type; + $data['image_output_format'] = ( $output_mime !== $mime_type ) ? $output_mime : null; + } - /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ - $data['image_save_progressive'] = (bool) apply_filters( 'image_save_progressive', false, $mime_type ); + // Per-file progressive/interlaced encoding flag for images, evaluated + // against the attachment's MIME type. + if ( in_array( 'image_save_progressive', $fields, true ) ) { + /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */ + $data['image_save_progressive'] = (bool) apply_filters( 'image_save_progressive', false, $mime_type ); + } } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 4951bc1341275..03249a008ba7d 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2062,13 +2062,17 @@ public function test_image_output_format_with_custom_filter(): void { $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); $captured = array(); - $filter = static function ( $formats, $filename, $mime_type ) use ( &$captured ) { - $captured['filename'] = $filename; - $captured['mime_type'] = $mime_type; - $formats['image/jpeg'] = 'image/webp'; - return $formats; - }; - add_filter( 'image_editor_output_format', $filter, 10, 3 ); + add_filter( + 'image_editor_output_format', + static function ( $formats, $filename, $mime_type ) use ( &$captured ) { + $captured['filename'] = $filename; + $captured['mime_type'] = $mime_type; + $formats['image/jpeg'] = 'image/webp'; + return $formats; + }, + 10, + 3 + ); $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); $request->set_param( 'context', 'edit' ); @@ -2098,10 +2102,14 @@ public function test_image_save_progressive_with_custom_filter(): void { $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); - $filter = static function ( $progressive, $mime_type ) { - return 'image/jpeg' === $mime_type; - }; - add_filter( 'image_save_progressive', $filter, 10, 2 ); + add_filter( + 'image_save_progressive', + static function ( $progressive, $mime_type ) { + return 'image/jpeg' === $mime_type; + }, + 10, + 2 + ); $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); $request->set_param( 'context', 'edit' );