Skip to content

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

Open
adamsilverstein wants to merge 35 commits into
WordPress:trunkfrom
adamsilverstein:add/heic-canvas-fallback
Open

Media: Enable HEIC/HEIF uploads when server lacks image editor support#11323
adamsilverstein wants to merge 35 commits into
WordPress:trunkfrom
adamsilverstein:add/heic-canvas-fallback

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Mar 20, 2026

Copy link
Copy Markdown
Member

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

Note: merge after client side media is restored in core.

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: WordPress/gutenberg#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.

A new original-heic sideload image-size token records the source-format original under a dedicated source_image metadata key (so it never collides with original_image), and wp_delete_attachment_heic_companion_file() cleans that companion file up on attachment delete.

Behavior note

The HEIC bypass is unconditional for the HEIC/HEIF mime family — it does not require generate_sub_sizes === false. As a result, a non-browser REST client uploading HEIC to a server without HEIC support now receives a stored attachment with empty missing_image_sizes instead of the previous rest_upload_image_type_not_supported error. This is intentional: the browser can always decode HEIC client-side, and raw API consumers can generate sub-sizes themselves. Flagging it as a deliberate contract change for POST /wp/v2/media.

Related

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)

Bypass the `wp_prevent_unsupported_mime_type_uploads` check
for HEIC/HEIF images so they can be stored even when the
server's image editor doesn't support them. The client-side
canvas fallback handles processing using the browser's
native HEVC decoder via createImageBitmap().
@github-actions

github-actions Bot commented Mar 20, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein, westonruter, ramonopoly.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@adamsilverstein adamsilverstein changed the title Media: Allow HEIC/HEIF uploads when server lacks image editor support Media: Enable HEIC/HEIF uploads when server lacks image editor support Mar 20, 2026
…back

# Conflicts:
#	src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
@adamsilverstein

adamsilverstein commented Apr 22, 2026

Copy link
Copy Markdown
Member Author

Note for follow-up: when r62081 (commit c863860, "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):

// When the client handles image processing (generate_sub_sizes is false),
// skip the server-side image editor support check.
if ( false === $request['generate_sub_sizes'] ) {
    $prevent_unsupported_uploads = false;
}

@ramonjd

ramonjd commented May 12, 2026

Copy link
Copy Markdown
Member

Heads up, there was a follow up to WordPress/gutenberg#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

Copy link
Copy Markdown
Member Author

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!

…back

# Conflicts:
#	src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
Extends the /wp/v2/media/<id>/sideload route so the client-side media
flow can upload a HEIC/HEIF companion original alongside the JPEG
derivative:

- Adds 'original-heic' to the allowed image_size enum. The companion
  filename is recorded under $metadata['original'] so it never
  collides with 'original_image', which the scaled-sideload flow owns.
- Adds a 'generate_sub_sizes' boolean arg (default false) so callers
  that handle processing client-side can suppress server-side sub-size
  generation per request.
- Adds 'image/heif' to the image_output_formats input list returned by
  the REST API root index.

Backport of GB #76731.
When the client-side media flow sideloads a HEIC original alongside a
JPEG derivative, the HEIC filename is stored in $metadata['original'].
wp_delete_attachment_files() only tracks 'original_image', so without
this hook the HEIC file would linger on disk after the attachment is
removed.

wp_delete_attachment_heic_companion_file() reads the meta key, guards
against non-string values (e.g. arrays written by other flows), and
deletes the file when present. Hooked into the delete_attachment
action via default-filters.php.

Backport of GB #76731, with the is_string() guard from GB #78128.
Adds REST API controller tests:
- The sideload route exposes 'original-heic' in the image_size enum.
- The sideload route exposes a 'generate_sub_sizes' boolean arg
  defaulting to false.
- Sideloading an 'original-heic' image writes the filename to
  $metadata['original'] and leaves 'original_image' untouched.

Adds wp_delete_attachment_heic_companion_file() unit tests:
- The companion HEIC is removed when the attachment is deleted.
- The hook is a no-op when $metadata['original'] is missing.
- The hook bails when $metadata['original'] is not a string
  (regression coverage for the guard added in GB #78128).
@adamsilverstein adamsilverstein force-pushed the add/heic-canvas-fallback branch from 707d121 to b20bbca Compare May 28, 2026 16:12
adamsilverstein and others added 5 commits May 28, 2026 12:39
…deload.

The test was sending JPEG bytes with a .heic filename, which wp_check_filetype_and_ext() corrected to canola-1.jpg before the metadata assertion ran. Switch to the real test-image.heic fixture, set Content-Type accordingly, and pass convert_format=false to disable the default HEIC -> JPEG output mapping so the .heic extension is preserved.
Add 'original-heic' to the image_size enum and the missing generate_sub_sizes arg so the schema fixture matches what the live REST index now reports. Without this the test-fixtures step fails the git diff --exit-code check.
Comment thread src/wp-includes/media.php Outdated
Comment thread src/wp-includes/media.php Outdated
Comment thread src/wp-includes/media.php Outdated

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from Claude Code (/code-review xhigh-effort pass), posted by @westonruter. Findings left inline; the bottom half are cleanup/altitude notes rather than bugs — dismiss anything that doesn't land.

Comment thread src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Outdated
Comment thread src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Outdated
Comment thread src/wp-includes/media.php
Comment thread src/wp-includes/rest-api/class-wp-rest-server.php Outdated
Comment thread src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php Outdated
Comment thread src/wp-includes/media.php Outdated
Comment thread tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php Outdated
adamsilverstein and others added 2 commits May 28, 2026 15:01
Co-authored-by: Weston Ruter <westonruter@gmail.com>
Harden the companion-original handling surfaced in review:

- Rename the companion metadata key from the over-generic 'original' to
  'source_image' so unrelated plugin or theme data stored under 'original'
  can no longer drive file deletion on attachment delete.
- Add IMAGE_SIZE_SOURCE_ORIGINAL and META_KEY_SOURCE_IMAGE class constants
  so the sideload image_size enum and its dispatch branch cannot drift.
- Drop the unused generate_sub_sizes argument from the /sideload route
  schema; only create_item() reads it, so advertising it on sideload
  silently misleads clients.
- Advertise the HEIC/HEIF -sequence variants in the REST index input
  formats so they match wp_is_heic_image_mime_type().
- Return a boolean from wp_delete_attachment_heic_companion_file() and
  strengthen the non-string guard test with a real on-disk bystander file
  so the regression it protects against can actually fail.
@adamsilverstein adamsilverstein force-pushed the add/heic-canvas-fallback branch from da64fe4 to 7f976bb Compare May 28, 2026 20:51
Co-authored-by: Weston Ruter <westonruter@gmail.com>
adamsilverstein added a commit to WordPress/gutenberg that referenced this pull request Jun 18, 2026
* Media: Rename HEIC companion metadata key to 'source_image'

During the wordpress-develop backport review (WordPress/wordpress-develop#11323),
the metadata key recording the sideloaded source-format original was renamed from
the generic 'original' to the dedicated 'source_image', and the size token and the
key were promoted to controller constants so the sideload schema and the metadata
writer cannot drift apart. Port those changes back so the plugin and core agree on
the key, and align the delete_attachment cleanup hook with the core implementation.

Add PHPUnit coverage for the renamed key and the cleanup hook.

* Add backport changelog entry linking to core PR #11323
peterwilsoncc pushed a commit to peterwilsoncc/gutenberg-build that referenced this pull request Jun 18, 2026
* Media: Rename HEIC companion metadata key to 'source_image'

During the wordpress-develop backport review (WordPress/wordpress-develop#11323),
the metadata key recording the sideloaded source-format original was renamed from
the generic 'original' to the dedicated 'source_image', and the size token and the
key were promoted to controller constants so the sideload schema and the metadata
writer cannot drift apart. Port those changes back so the plugin and core agree on
the key, and align the delete_attachment cleanup hook with the core implementation.

Add PHPUnit coverage for the renamed key and the cleanup hook.

* Add backport changelog entry linking to core PR #11323

Source: WordPress/gutenberg@acb09cb
@westonruter

Copy link
Copy Markdown
Member

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.

Comment thread src/wp-includes/post.php Outdated
* width: int<1, max>,
* height: int<1, max>,
* file: non-empty-string,
* source_image?: non-empty-string,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This source_image key is newly introduced in this PR.

westonruter and others added 6 commits June 18, 2026 17:37
… types

The @phpstan-return shape previously required width, height, file, and
sizes, which only holds for raster images. PDFs expose just sizes and
filesize, while audio/video attachments produce an entirely different
set of keys. Mark those keys optional so non-image attachments are typed
correctly, and add the documented-but-missing filesize and image_meta
keys. Unseal the per-size arrays and add their filesize key as well,
since size items ride through the same metadata filters and can carry
plugin-added keys such as modern-format sources.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tself

test_sideload_route_includes_and_excludes_expected_fields treated the value
returned by array_first( $routes[ $path ] ) as the args map, but that value
is the route endpoint whose argument definitions live under its 'args' key.
As a result assertArrayHasKey( 'image_size', ... ) failed on every PHP
version, turning the whole PHPUnit matrix red. Descend into ['args'] first,
matching the structure the sibling scaled-enum assertions already relied on.
@adamsilverstein

Copy link
Copy Markdown
Member Author

This should be ready to go.

Move the `@phpstan-return`/`@phpstan-param` annotations and the
phpstan-phpunit extension out of this feature branch and into PR WordPress#12313 so
the static-analysis work can be reviewed independently of the HEIC upload
feature, per code-review feedback.

Also correct the `$posts_clauses` test docblock: each recorded entry is the
array of SQL clause fragments from the `posts_clauses` filter, so the type is
`array[]`, not `string[]`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants