Add JPEG XL support, loaded on demand#77584
Conversation
Add client-side JPEG XL support without growing the vips worker bundle. The 3 MB vips-jxl.wasm module is split into its own @wordpress/vips/jxl-wasm script module that is only fetched the first time a JXL image is processed. How it works: - packages/vips/src/jxl-wasm.ts is a tiny new script module whose only job is to export the base64 data URL for vips-jxl.wasm. - @wordpress/vips/jxl-wasm is registered as a new wpScriptModuleExports entry so the build emits build/modules/vips/jxl-wasm.min.js (~3 MB) as a separately loadable module. - vips-worker.ts adds vipsEnsureJxlSupport(), which on first call dynamic-imports '@wordpress/vips/jxl-wasm' to get the data URL, then RPC-passes it to the worker via setJxlWasmUrl(). - The worker (packages/vips/src/index.ts) stores the URL, adds vips-jxl.wasm to dynamicLibraries on the next getVips() call, and returns the URL from locateFile(). If vips was already initialized without JXL, the existing instance is discarded so the reinit picks up JXL support. - packages/upload-media calls vipsEnsureJxlSupport() from prepareItem whenever the input or output type is image/jxl. image/jxl is added to CLIENT_SIDE_SUPPORTED_MIME_TYPES and 'jxl' to VALID_IMAGE_FORMATS and the ImageFormat type. The vipsConvertImageFormat wrapper MIME union is widened accordingly. - JXL encoding uses effort=3 (libvips default 7 is too slow for interactive use). Size impact: - worker.min.js: unchanged (~13.1 MB, same as trunk). - jxl-wasm.min.js: new separate module (~3.0 MB), fetched only when a JXL image is encountered. Alternative to #77570, which bundles vips-jxl.wasm directly into the worker (+3 MB on every editor page load). Opened so the size bot can compare. Refs #76981.
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +1.1 MB (+13.71%) Total Size: 9.14 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Flaky tests detected in 96125ce. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/26313133000
|
Resolve conflict in packages/upload-media/src/store/private-actions.ts: trunk moved image format transcoding from client-side prepareItem to server-driven thumbnail generation. Adopt the new pipeline and keep the JXL WASM lazy-load, but trigger it in two places: prepareItem for JXL input files, and transcodeImageItem for server-requested JXL output.
|
A few notes from a review pass: Bundle size / lighter-weight JXL options (re #76981)
Minor code note
|
JXL is not broadly web-compatible: most browsers (including Chrome) cannot display it and the server cannot read it (GD/Imagick have no JXL decoder, and fileinfo reports it as image/x-jxl). Uploading JXL as-is was rejected outright — by the editor's allowed-MIME check and by core's wp_check_filetype_and_ext() — and even when allowed produced an undisplayable attachment with no dimensions or sub-sizes. Decode JXL to JPEG client-side with vips (the JXL WASM module is already lazy-loaded on demand) and upload the JPEG, mirroring how HEIC is handled. The original .jxl is preserved as a companion file in $metadata['original'] so no data is lost. The editor and front end now use the portable JPEG, with real dimensions and the full set of sub-sizes. - Register image/jxl as an allowed upload MIME type, and restore the type during validation via a magic-byte-checked wp_check_filetype_and_ext filter so the sideloaded original passes despite the image/x-jxl finfo mismatch. - Add original-jxl sideload handling to the REST controller, skipping the dimension read since JXL cannot be measured server-side. - Generalize the HEIC companion delete hook to clean up JXL originals too. - Add an e2e test and JXL asset covering the conversion and companion.
# Conflicts: # packages/upload-media/src/store/private-actions.ts
|
I did some testing on this feature, made some small fixes and got it working, it now properly handles uploaded JXL files. I created a JXL using Squoosh for testing: Because JPEG XL (JXL) is not supported in Chromium where client-side media is active, and generally to provide a web-safe format for users, I decided to treat JXL uploads similar to HEIC uploads. So we do the following:
The output format should also honor the See screencast: jxl.to.jpeg.on.upload.mp4 |
|
I will work on a core backport once the core client-side media feature is restored. |
# Conflicts: # packages/vips/CHANGELOG.md # packages/vips/README.md
Summary
Third experimental approach to #76981: lazy-load the 3 MB
vips-jxl.wasmmodule only when a user first processes a JXL image. No canonical plugin required, and no bundle-size hit on editor pages that never touch JXL.Sibling of:
vips-jxl.wasmdirectly (adds ~3 MB to every editor load).wp-vips-jxl) that ships the WASM on demand.This PR keeps the WASM inside Gutenberg but splits it into its own script module chunk (
@wordpress/vips/jxl-wasm) that the browser only fetches on first JXL use.Fixes #76981.
How it works
packages/vips/src/jxl-wasm.tsexports the base64 data URL forvips-jxl.wasm. Added towpScriptModuleExportsinpackages/vips/package.jsonso the build emits a standalonebuild/modules/vips/jxl-wasm.min.js(~3.0 MB).vipsEnsureJxlSupport()inpackages/vips/src/vips-worker.tsdynamic-imports@wordpress/vips/jxl-wasmand RPC-passes the URL to the worker viasetJxlWasmUrl(). The import and RPC are cached, so subsequent calls are no-ops.vips-jxl.wasmtodynamicLibrarieson the nextgetVips()call. If vips was already initialized without JXL, the existing instance is discarded and recreated with JXL support.locateFilereturns the URL when vips requestsvips-jxl.wasm.packages/upload-mediacallsvipsEnsureJxlSupport()fromprepareItemwhenever the input or output type isimage/jxl.image/jxlis added toCLIENT_SIDE_SUPPORTED_MIME_TYPES,'jxl'toVALID_IMAGE_FORMATSandImageFormat, and thevipsConvertImageFormatwrapper MIME union. JXL encoding useseffort=3(libvips default of 7 is too slow for interactive use).@wordpress/vips/workercorrectly declares a dynamicmodule_dependenciesentry on@wordpress/vips/jxl-wasm, so WordPress's import map resolves it at runtime.Screencast
jxl.to.jpeg.on.upload.mp4
Bundle size impact
Measured locally from
npm run build. Raw is the on-disk minified size; transferred (gzip) is what the browser actually downloads and what the CI size bot reports:build/modules/vips/worker.min.jsbuild/modules/vips/jxl-wasm.min.jsThe CI size bot's headline
+1.1 MB (+13.83%)is the gzipped JXL chunk, not the 3.0 MB raw figure — the actual network cost is smaller than the on-disk size. Editor sessions that never process a JXL image transfer no extra bytes (the worker grows by only 262 B). When a user uploads a JXL image, the browser fetches the separate chunk (~1.1 MB gzip) once and caches it.Comparison matrix
Test plan
npm run buildproducesbuild/modules/vips/worker.min.jsat ~13.1 MB and a separatebuild/modules/vips/jxl-wasm.min.jsat ~3.0 MB.module_dependenciesentry on@wordpress/vips/jxl-wasm.jxl-wasm.min.js..jxlfile: the browser fetchesjxl-wasm.min.jsonce, then processes the image client-side (resize, compress, thumbnails).Refs #76981.