Make WordPress Core

Opened 6 months ago

Closed 3 months ago

Last modified 3 months ago

#64915 closed defect (bug) (fixed)

Media: Enable HEIC/HEIF uploads when server lacks image editor support

Reported by: adamsilverstein Owned by: adamsilverstein
Priority: normal Milestone: 7.1
Component: Media Version: 7.0
Severity: normal Keywords: has-patch gutenberg-merge commit has-unit-tests
Cc: Focuses:

Description

Description

HEIC (HEIF with HEVC codec) is the default photo format on iPhones. When users upload HEIC images via the REST API and the server's image editor (ImageMagick/GD) doesn't support HEIC, the upload is blocked entirely by the wp_prevent_unsupported_mime_type_uploads check in WP_REST_Attachments_Controller::create_item_permissions_check(), returning a rest_upload_image_type_not_supported error.

With the client-side media processing feature, the browser can decode HEIC images natively using createImageBitmap() — which leverages the OS/browser's licensed HEVC codecs (Safari everywhere, Chrome on macOS, Chrome on Windows with codec extension) — and convert them to JPEG for sub-size generation via canvas. However, this requires the server to first accept the HEIC upload.

Proposed Change

Bypass the $prevent_unsupported_uploads check for HEIC/HEIF MIME types (detected via the existing wp_is_heic_image_mime_type() helper) so the file can be stored on the server even when the image editor can't process it.

The flow becomes:

  1. Upload HEIC to server — server tries to process and generate sub-sizes
  2. If server has HEIC support → everything works as before
  3. If server lacks HEIC support → file is stored, missing_image_sizes is populated in the REST response
  4. Client detects missing sizes and uses browser-native createImageBitmap() + OffscreenCanvas to decode and convert to JPEG
  5. JPEG is sideloaded as the scaled version, sub-sizes are generated from the JPEG via the existing client-side resize pipeline

This approach:

  • Is backwards compatible (servers with HEIC support are unaffected)
  • Leverages browser-licensed HEVC decoding rather than shipping a decoder
  • Uses the existing wp_is_heic_image_mime_type() function already in core

Change Summary

In src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php:

// Always allow HEIC/HEIF uploads through even if the server's image
// editor doesn't support them. The client-side canvas fallback will
// handle processing using the browser's native HEVC decoder.
if (
    $prevent_unsupported_uploads &&
    ! empty( $files['file']['type'] ) &&
    wp_is_heic_image_mime_type( $files['file']['type'] )
) {
    $prevent_unsupported_uploads = false;
}

Testing

  1. Use a WordPress environment where ImageMagick does NOT have HEIC support
  2. Upload a HEIC image via the REST API media endpoint (POST /wp/v2/media)
  3. Verify the upload succeeds (previously returned rest_upload_image_type_not_supported error)
  4. Verify missing_image_sizes is populated in the response

Change History (19)

#1 @adamsilverstein
6 months ago

  • Owner set to adamsilverstein
  • Status newassigned

This ticket was mentioned in PR #11323 on WordPress/wordpress-develop by @adamsilverstein.


6 months ago
#2

  • Keywords has-patch added

Trac ticket: https://core.trac.wordpress.org/ticket/64915

## What?

Allow HEIC/HEIF image uploads to succeed even when the server's image editor (ImageMagick/GD) doesn't support HEIC. Currently, wp_prevent_unsupported_mime_type_uploads blocks these uploads entirely.

Gutenberg PR: https://github.com/WordPress/gutenberg/pull/76731

## Why?

HEIC is the default photo format on iPhones. When users upload HEIC images and the server can't process them, the upload fails with an unhelpful error. With the client-side media processing feature, the browser can decode HEIC using its native createImageBitmap() API (leveraging OS-licensed HEVC codecs) and convert to JPEG for sub-size generation. But first, the server needs to accept the upload.

## How?

In WP_REST_Attachments_Controller::create_item_permissions_check(), bypass the $prevent_unsupported_uploads check when the uploaded file is HEIC/HEIF (detected via the existing wp_is_heic_image_mime_type() helper). This allows the file to be stored on the server, after which the client-side canvas fallback generates a JPEG version and all required sub-sizes.

## Testing Instructions

  1. Use a WordPress environment where ImageMagick does NOT have HEIC support
  2. Upload a HEIC image via the REST API media endpoint
  3. Verify the upload succeeds (previously would return rest_upload_image_type_not_supported error)
  4. Verify missing_image_sizes is populated in the response (server couldn't generate sub-sizes)

@adamsilverstein commented on PR #11323:


5 months ago
#3

Note for follow-up: when r62081 (commit c863860ccf, "Media: Remove client-side media processing feature for now") is reverted to reintroduce client-side media processing in 7.1, the generate_sub_sizes === false skip in WP_REST_Attachments_Controller::create_item_permissions_check() should be restored alongside the HEIC/HEIF allowance added here.

Original block from this PR (dropped during the trunk merge since generate_sub_sizes no longer exists on trunk):

\\\`php
When the client handles image processing (generate_sub_sizes is false),
skip the server-side image editor support check.
if ( false === \$requestgenerate_sub_sizes ) {

\$prevent_unsupported_uploads = false;

}
\\\`

@ramonopoly commented on PR #11323:


4 months ago
#4

Heads up, there was a follow up to https://github.com/WordPress/gutenberg/pull/76731:

Adds an is_string() guard to gutenberg_delete_heic_companion_file() so it bails on attachments whose wp_get_attachment_metadata()original is not a string filename.

Not sure if it needs a core sync at all since it's a GB load.php change. 🤔

@adamsilverstein commented on PR #11323:


4 months ago
#5

Not sure if it needs a core sync at all since it's a GB load.php change

Possibly, I will review once the 7.1 branch is open and I am able to reintroduce the feature in core.

Thanks for cross linking this ticket!

#6 @audrasjb
4 months ago

Removing trunk version as this is not going to be shipped with WP 7.0 but in the next releases.

@westonruter commented on PR #11323:


3 months ago
#7

The changes in this PR now pass PHPStan rule level 10. I can cherry-pick the PHPStan-specific changes into a new core commit for Core-64898 so there's less to commit here. But I wanted to include the type definitions to reference with reviewing.

@adamsilverstein commented on PR #11323:


3 months ago
#8

This should be ready to go.

This ticket was mentioned in PR #12313 on WordPress/wordpress-develop by @adamsilverstein.


3 months ago
#9

## What?

Adds @phpstan-return/@phpstan-param annotations describing the array shapes returned and accepted by several upload and attachment helpers, and loads the `phpstan/phpstan-phpunit` type-specifying extension so PHPUnit assertions (e.g. assertIsArray(), assertInstanceOf()) narrow types during analysis. Only the extension is included, not its rules.neon, so no new strict rules are introduced.

Annotated functions:

  • _wp_handle_upload(), wp_handle_upload(), wp_handle_sideload()
  • wp_get_upload_dir(), wp_upload_dir()
  • wp_get_attachment_metadata()
  • WP_REST_Request::get_file_params() / set_file_params()

## Why?

These changes are documentation/tooling only, with no runtime effect. They let core pass a higher PHPStan rule level on the affected functions.

This work was extracted from the HEIC client-side upload PR (#11323) per code-review feedback, so the static-analysis improvements can be reviewed and landed independently of the feature. When both this and #11323 land, the affected code passes PHPStan rule level 10.

## Notes

  • No composer.lock change is included (matching the originating PR). A follow-up composer update phpstan/phpstan-phpunit may be wanted to pin the lock; core CI does not currently gate on PHPStan or lock consistency.
  • May warrant its own Trac ticket for the code-quality milestone; currently associated with the HEIC work under #64915.

## Testing Instructions

  1. composer install
  2. Run PHPStan: composer phpstan (or the project's configured command).
  3. Confirm no new errors are reported and the annotated functions analyze at the higher level.

#10 @wildworks
3 months ago

  • Keywords gutenberg-merge added

@adamsilverstein commented on PR #11323:


3 months ago
#11

I opened up a follow up to handle -sequence images (live or burst) - https://github.com/WordPress/gutenberg/issues/79642

@adamsilverstein commented on PR #11323:


3 months ago
#12

This is looking good to me! The only thing I wondered while looking over this backport (similar to #12005) is whether the image size is best hyphenated or separated via underscores. I don't mind which, just wasn't sure the convention as the values in metadata (e.g. source_image use underscores).

Good point about the naming convention. I feel like we have primarily used underscores or database names and dashes for image size names. I'm going to review a bit more and then maybe open a follow up issue to discuss changing, I don't want to tackle that in this PR.

@adamsilverstein commented on PR #11323:


3 months ago
#13

This is looking good to me! The only thing I wondered while looking over this backport (similar to #12005) is whether the image size is best hyphenated or separated via underscores. I don't mind which, just wasn't sure the convention as the values in metadata (e.g. source_image use underscores).

Good point about the naming convention. I feel like we have primarily used underscores or database names and dashes for image size names. I'm going to review a bit more and then maybe open a follow up issue to discuss changing, I don't want to tackle that in this PR.

@andrewserong reviewed this again, I think we can just drop the "original-heic" naming altogether and stick to source_image. Its just a holdover for what we used originally. I have a PR in progress to bring changes back to Gutenberg from these backport PRs, I'll change it on the Gutenberg side there - https://github.com/WordPress/gutenberg/pull/79603

@adamsilverstein commented on PR #11323:


3 months ago
#14

@andrewserong[[Image(chrome-extension://hgomfjikakokcbkjlfgodhklifiplmpg/images/wp-logo.png)]] reviewed this again, I think we can settle on the underscore naming. I have a PR in progress to bring changes back to Gutenberg from these backport PRs, I'll change it on the Gutenberg side there - WordPress/gutenberg#79603

I verified HEIC uploads still work even after the renaming, only the original upload fails to work, which feels fine for trunk. We'll just want to land the Gutenberg PR with the matching changes quickly so it gets pulled over by beta1.

#15 @adamsilverstein
3 months ago

  • Keywords commit added

#16 @adamsilverstein
3 months ago

  • Resolutionfixed
  • Status assignedclosed

In 62616:

Media: Allow HEIC/HEIF uploads when the server lacks editor support.

Enable HEIC/HEIF image uploads to succeed even when the server's image editor cannot process them, so client-side media processing can decode the file in the browser and generate the required sub-sizes. Previously, wp_prevent_unsupported_mime_type_uploads rejected these uploads outright with a rest_upload_image_type_not_supported error.

See related Gutenberg work: https://github.com/WordPress/gutenberg/pull/76731.

Props westonruter, swissspidy, ramonopoly.
Fixes #64915.

#17 @adamsilverstein
3 months ago

In 62618:

Code Quality: Add PHPStan type coverage for media and upload functions.

Add @phpstan-return/@phpstan-param annotations describing the array shapes returned and accepted by various media files. Also load the phpstan/phpstan-phpunit extension so PHPUnit assertions narrow types during analysis.

These changes are documentation and tooling only, with no runtime effect, and let the affected functions pass a higher PHPStan rule level.

Props westonruter.
See #64915.

This ticket was mentioned in PR #12002 on WordPress/wordpress-develop by @adamsilverstein.


3 months ago
#19

  • Keywords has-unit-tests added

## What

Adds the convert_format boolean-arg test coverage from Gutenberg #77565 — Media uploading: declare `convert_format` as boolean arg on sideload route.

Part of the post-restore client-side-media backport stack (see #11324).

Note: This PR originally backported the GB #75888 finalize-metadata refactor (Trac #65329). That production code and its finalize tests have since landed in trunk, so the PR has been reduced to the remaining convert_format test coverage. Diff shown against trunk for standalone-mergeability.

## Why

The sideload handler reads $request['convert_format'], and the client sends convert_format: false in additionalData when uploading a HEIC companion. With multipart/form-data, an undeclared arg arrives as the string "false", which is truthy in PHP - so if ( ! $request['convert_format'] ) never fires and add_filter( 'image_editor_output_format', '__return_empty_array', 100 ) is skipped. The default HEIC->JPEG output mapping then survives and wp_unique_filename()'s alt-extension collision check bumps the original to -1 while the JPEG derivative stays unsuffixed, so the two companion files drift apart on repeat uploads. Declaring the arg as boolean lets REST coerce "false" -> PHP false, the filter fires, and the companions keep a shared basename.

## How

The controller change is already in Core. When client-side media processing was reintroduced ([61703]), the sideload route already declared convert_format as a boolean (default true) and sideload_item() already suppresses image_editor_output_format when it is false. So this backport is tests only - it adds the coverage from GB #77565 to tests/phpunit/tests/rest-api/rest-attachments-controller.php:

  • test_sideload_route_declares_convert_format_boolean - asserts the sideload route declares convert_format as a boolean defaulting to true.
  • test_sideload_convert_format_false_suppresses_alt_ext_suffix - simulates the HEIC companion flow (PNG stand-in + a local PNG->JPEG mapping) and verifies that passing convert_format as the string "false" keeps the companion's shared basename with no numeric suffix.

## Notes

  • Adapted to Core conventions: self::$author_id, self::$test_file, $this->enable_client_side_media_processing(), and : void-style test setup. The behavior test uses image_size => 'original' (a valid enum value) rather than the Gutenberg test's 'original-heic', since Core's image_size validation strictly enforces the registered-size enum.
  • Validated locally with php -l and PHPCS (WordPress-Core).


## Proposed commit message

64915

REST API: Add test coverage for the sideload `convert_format` boolean arg.

The sideload route already declares `convert_format` as a boolean (default true) and `sideload_item()` suppresses the `image_editor_output_format` filter when it is false, both added when client-side media processing was reintroduced in [61703]. This adds the regression coverage that was still missing.

Without the boolean declaration, a `multipart/form-data` request delivers `convert_format` as the string "false", which is truthy in PHP, so `if ( ! $request['convert_format'] )` never fires and the opted-out format conversion still runs - bumping a companion file to a `-1` suffix while its derivative stays unsuffixed.

Add two tests to the REST attachments controller suite:

* `test_sideload_route_declares_convert_format_boolean` asserts the route declares `convert_format` as a boolean defaulting to true.
* `test_sideload_convert_format_false_suppresses_alt_ext_suffix` simulates the HEIC companion flow (a PNG stand-in plus a local PNG-to-JPEG output mapping) and verifies that passing `convert_format` as the string "false" keeps the companion's shared basename with no numeric suffix.

See https://github.com/WordPress/gutenberg/pull/77565.

Props adamsilverstein, andrewserong.
See #64915.
Note: See TracTickets for help on using tickets.