#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:
- Upload HEIC to server — server tries to process and generate sub-sizes
- If server has HEIC support → everything works as before
- If server lacks HEIC support → file is stored,
missing_image_sizesis populated in the REST response - Client detects missing sizes and uses browser-native
createImageBitmap()+OffscreenCanvasto decode and convert to JPEG - JPEG is sideloaded as the
scaledversion, 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
- Use a WordPress environment where ImageMagick does NOT have HEIC support
- Upload a HEIC image via the REST API media endpoint (
POST /wp/v2/media) - Verify the upload succeeds (previously returned
rest_upload_image_type_not_supportederror) - Verify
missing_image_sizesis populated in the response
Related
- Core PR: https://github.com/WordPress/wordpress-develop/pull/11323
- Gutenberg PR: https://github.com/WordPress/gutenberg/pull/76731
- Gutenberg issue: https://github.com/WordPress/gutenberg/issues/76732
Change History (19)
This ticket was mentioned in PR #11323 on WordPress/wordpress-develop by @adamsilverstein.
6 months ago
#2
- Keywords has-patch added
@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
@
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.lockchange is included (matching the originating PR). A follow-upcomposer update phpstan/phpstan-phpunitmay 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
composer install- Run PHPStan:
composer phpstan(or the project's configured command). - Confirm no new errors are reported and the annotated functions analyze at the higher level.
@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_imageuse 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_imageuse 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.
@adamsilverstein commented on PR #12313:
3 months ago
#18
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_formattest 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 declaresconvert_formatas a boolean defaulting totrue.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 passingconvert_formatas 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 usesimage_size => 'original'(a valid enum value) rather than the Gutenberg test's'original-heic', since Core'simage_sizevalidation strictly enforces the registered-size enum. - Validated locally with
php -land 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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
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_uploadsblocks 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_uploadscheck when the uploaded file is HEIC/HEIF (detected via the existingwp_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
rest_upload_image_type_not_supportederror)missing_image_sizesis populated in the response (server couldn't generate sub-sizes)