Media: Add JPEG XL (JXL) upload support with companion-original preservation (backport GB #77584)#12006
Draft
adamsilverstein wants to merge 9 commits into
Draft
Conversation
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().
…back # Conflicts: # src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
…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).
…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.
…rvation. Add server-side support for the client-side media flow that decodes JPEG XL uploads to JPEG in the browser via lazy-loaded VIPS/WASM and preserves the original .jxl as a companion file (under $metadata['original'], the same slot the HEIC companion uses). * Register `image/jxl` as an allowed upload MIME type. WordPress core does not include JXL in its default MIME list, so without this filter the editor's allowed-types check rejects the original .jxl before it can be converted. * Add `wp_is_jxl_file()`, a magic-bytes check that recognizes both JXL flavors (the naked codestream beginning 0xFF 0x0A and the 12-byte ISOBMFF container signature). * Add `wp_filter_jxl_filetype_and_ext` on `wp_check_filetype_and_ext`. PHP's fileinfo reports JXL as `image/x-jxl` (and getimagesize() can't identify it at all), so core's MIME-mismatch check otherwise rejects the upload. When the file is genuinely JXL, restore the expected ext/type so the upload is allowed. * Accept `original-jxl` as a sideload `image_size`. The branch is collapsed with `original-heic` since both write to $metadata['original'] and are mutually exclusive. * Rename `wp_delete_attachment_heic_companion_file()` to `wp_delete_attachment_preserved_original_companion_file()` since the same hook now covers both HEIC and JXL companions. The implementation is unchanged; only the name and docblock are generalized. The test file is renamed alongside. Backport of WordPress/gutenberg#77584. Stacked on WordPress#11323 (HEIC canvas fallback).
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
image/jxlas an allowed upload MIME type.wp_is_jxl_file(), a magic-bytes check that recognizes both JXL flavors (naked codestream + ISOBMFF container).wp_filter_jxl_filetype_and_ext()onwp_check_filetype_and_extto restore the canonicalimage/jxlMIME when PHP'sfileinforeports the non-canonicalimage/x-jxlform.original-jxlas a sideloadimage_size. The HEIC and JXL branches insideload_item()are collapsed since both write to$metadata['original']and are mutually exclusive.wp_delete_attachment_heic_companion_file()→wp_delete_attachment_preserved_original_companion_file()since the samedelete_attachmenthook now covers both HEIC and JXL companions. Implementation unchanged; only the name and docblock are generalized. The test file is renamed alongside, and a JXL-companion deletion test is added.Why
JPEG XL is a modern image format that can losslessly transcode existing JPEGs at significantly smaller size. WordPress core does not recognize
.jxluploads today:image/jxlis not in the defaultupload_mimeslist, so the upload is rejected before any conversion can happen.getimagesize()cannot identify JXL files. PHP'sfileinfoextension reports them asimage/x-jxl(not the registeredimage/jxl), sowp_check_filetype_and_ext()flags the upload as MIME-mismatched and rejects it.When client-side media processing is enabled, the browser decodes the uploaded JXL to a JPEG using a lazy-loaded VIPS/WASM module and uploads the JPEG as the primary attachment. The original
.jxlis preserved as a companion file under$metadata['original'], exactly like the HEIC companion-original pattern in #11323.This PR is the server-only backport of WordPress/gutenberg#77584. The lazy WASM loader, VIPS worker, store actions, and editor UI ship through the normal Gutenberg → Core package sync.
Base
Stacked on #11323 (HEIC canvas fallback) — this PR generalizes the HEIC cleanup function so it can serve both formats. Merge #11323 first, then rebase this branch onto
trunk.Backport scope
src/wp-includes/media.phpwp_is_jxl_file( $file )— 12-byte magic-bytes check; recognizes both JXL flavors.wp_add_jxl_upload_mimes( $mimes )— adds'jxl' => 'image/jxl'.wp_filter_jxl_filetype_and_ext( $data, $file, $filename )— restoresimage/jxlwhen fileinfo returnedimage/x-jxl(or nothing) and the magic bytes confirm it.wp_delete_attachment_heic_companion_file()→wp_delete_attachment_preserved_original_companion_file(). Implementation identical; docblock generalized.src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.phpregister_routes()— addsoriginal-jxlto the sideloadimage_sizeenum.sideload_item()— collapses theoriginal-heicbranch withoriginal-jxl(both write to$metadata['original']).src/wp-includes/default-filters.phpadd_filter( 'upload_mimes', 'wp_add_jxl_upload_mimes' );add_filter( 'wp_check_filetype_and_ext', 'wp_filter_jxl_filetype_and_ext', 10, 3 );delete_attachmentaction updated to call the renamed function.Security model
The companion-file cleanup carries the existing HEIC hardening unchanged:
wp_basename()always strips path components from the recorded metadata before joining.wp_delete_file_from_directory()requires the resolved path to be a regular file strictly insidewp_get_upload_dir()['basedir'].The new magic-bytes check reads only the first 12 bytes via
fopen( 'rb' )/fread()and closes the handle immediately. It runs on the uploaded temp file before the file is moved into the uploads directory, so it never touches a user-controlled path.The
wp_filter_jxl_filetype_and_ext()filter is conservative: it only acts when core has already failed to identify the file (empty( $data['type'] )) AND the extension is.jxlAND the magic bytes match. A JPEG renamed to.jxlis not rescued.Test plan
vendor/bin/phpunit tests/phpunit/tests/media/wpJxlUpload.php— 10 new tests pass (magic-bytes for naked + ISOBMFF + JPEG + PNG + short file + missing file; upload_mimes filter; filetype filter happy path, already-recognized passthrough, and JXL-extension-with-wrong-magic rejection).vendor/bin/phpunit tests/phpunit/tests/media/wpDeleteAttachmentPreservedOriginalCompanionFile.php— existing HEIC tests still pass + new JXL companion deletion test passes..jxlfile. Confirm the JPEG derivative is the primary attachment and the.jxlis preserved on disk and recorded in_wp_attachment_metadata['original'].evil.jxland attempt to upload. Confirm the upload is rejected (magic bytes fail)..jxlare removed.Related