From f0e7ce370720f7492120e70af2b02e4277954583 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 22 Apr 2026 12:15:04 -0700 Subject: [PATCH] Client-side media: Bundle JPEG XL (JXL) support directly Bundle vips-jxl.wasm into the Gutenberg vips worker so client-side JPEG XL processing works out of the box without a separate plugin. - packages/vips: Re-add VipsJxlModule import, include vips-jxl.wasm in dynamicLibraries, wire it through locateFile, and set effort=3 on JXL saves (default of 7 is too slow for interactive use). Add image/jxl to supportsQuality. - packages/upload-media: Add image/jxl to CLIENT_SIDE_SUPPORTED_MIME_TYPES, add 'jxl' to VALID_IMAGE_FORMATS and the ImageFormat type, and widen the vipsConvertImageFormat wrapper's output type union. This is an alternative approach to #76990, which ships the same WASM module via a canonical plugin. Opening this PR to let the size bot quantify the bundle impact of shipping the module by default. Refs #76981. --- packages/upload-media/CHANGELOG.md | 4 ++++ packages/upload-media/src/store/constants.ts | 1 + .../upload-media/src/store/private-actions.ts | 9 ++++++- packages/upload-media/src/store/types.ts | 2 +- .../upload-media/src/store/utils/index.ts | 3 ++- packages/vips/CHANGELOG.md | 4 ++++ packages/vips/src/index.ts | 24 +++++++++++++++---- packages/vips/src/utils.ts | 17 +++++++++---- 8 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/upload-media/CHANGELOG.md b/packages/upload-media/CHANGELOG.md index 3c5b417053db4f..ab633a8dc6c6ac 100644 --- a/packages/upload-media/CHANGELOG.md +++ b/packages/upload-media/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Add `image/jxl` to `CLIENT_SIDE_SUPPORTED_MIME_TYPES` and `jxl` to the supported output formats, enabling client-side JPEG XL processing. + ### Enhancement - Remove sideload upload serialization: thumbnail uploads now run concurrently, governed by `maxConcurrentUploads` instead of being queued one-at-a-time per attachment ([#75257](https://github.com/WordPress/gutenberg/pull/75257)). diff --git a/packages/upload-media/src/store/constants.ts b/packages/upload-media/src/store/constants.ts index d71b6c1ed62499..963423d83906c9 100644 --- a/packages/upload-media/src/store/constants.ts +++ b/packages/upload-media/src/store/constants.ts @@ -27,6 +27,7 @@ export const CLIENT_SIDE_SUPPORTED_MIME_TYPES: readonly string[] = [ 'image/gif', 'image/webp', 'image/avif', + 'image/jxl', ] as const; /** diff --git a/packages/upload-media/src/store/private-actions.ts b/packages/upload-media/src/store/private-actions.ts index a48dfb70521035..999055f0b2fac7 100644 --- a/packages/upload-media/src/store/private-actions.ts +++ b/packages/upload-media/src/store/private-actions.ts @@ -566,7 +566,14 @@ export function finishOperation( }; } -const VALID_IMAGE_FORMATS = [ 'jpeg', 'webp', 'avif', 'png', 'gif' ] as const; +const VALID_IMAGE_FORMATS = [ + 'jpeg', + 'webp', + 'avif', + 'png', + 'gif', + 'jxl', +] as const; /** * Checks if a format string is a valid ImageFormat. diff --git a/packages/upload-media/src/store/types.ts b/packages/upload-media/src/store/types.ts index a12fc4f3b3cfd8..5ad73dea226cff 100644 --- a/packages/upload-media/src/store/types.ts +++ b/packages/upload-media/src/store/types.ts @@ -337,4 +337,4 @@ export interface SideloadAdditionalData extends AdditionalData { image_size: string | string[]; } -export type ImageFormat = 'jpeg' | 'webp' | 'avif' | 'png' | 'gif'; +export type ImageFormat = 'jpeg' | 'webp' | 'avif' | 'png' | 'gif' | 'jxl'; diff --git a/packages/upload-media/src/store/utils/index.ts b/packages/upload-media/src/store/utils/index.ts index 6109c85781211d..af5fc0cac0a3d3 100644 --- a/packages/upload-media/src/store/utils/index.ts +++ b/packages/upload-media/src/store/utils/index.ts @@ -59,7 +59,8 @@ export async function vipsConvertImageFormat( | 'image/png' | 'image/webp' | 'image/avif' - | 'image/gif', + | 'image/gif' + | 'image/jxl', quality: number, interlaced?: boolean ) { diff --git a/packages/vips/CHANGELOG.md b/packages/vips/CHANGELOG.md index 31eb7d798be4a4..cec559b0eeb9d1 100644 --- a/packages/vips/CHANGELOG.md +++ b/packages/vips/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Bundle `vips-jxl.wasm` to enable client-side JPEG XL (JXL) encoding and decoding. + ## 1.4.0 (2026-04-15) ## 1.3.0 (2026-04-01) diff --git a/packages/vips/src/index.ts b/packages/vips/src/index.ts index 3a2657bed4d6b5..4f52ef9a1935cd 100644 --- a/packages/vips/src/index.ts +++ b/packages/vips/src/index.ts @@ -9,6 +9,9 @@ import VipsModule from 'wasm-vips/vips.wasm'; // @ts-expect-error - WASM files are inlined as base64 data URLs at build time import VipsHeifModule from 'wasm-vips/vips-heif.wasm'; +// @ts-expect-error - WASM files are inlined as base64 data URLs at build time +import VipsJxlModule from 'wasm-vips/vips-jxl.wasm'; + /** * Internal dependencies */ @@ -41,10 +44,9 @@ async function getVips(): Promise< typeof Vips > { } vipsPromise = Vips( { - // Load HEIF dynamic module for HEIF/HEIC and AVIF format support. - // JXL is omitted as WordPress Core does not currently support it. - // It can be re-added when Core adds JXL support. - dynamicLibraries: [ 'vips-heif.wasm' ], + // Load HEIF dynamic module for HEIF/HEIC and AVIF format support, + // and JXL dynamic module for JPEG XL format support. + dynamicLibraries: [ 'vips-heif.wasm', 'vips-jxl.wasm' ], locateFile: ( fileName: string ) => { // WASM files are inlined as base64 data URLs at build time, // eliminating the need for separate file downloads and avoiding @@ -53,6 +55,8 @@ async function getVips(): Promise< typeof Vips > { return VipsModule; } else if ( fileName.endsWith( 'vips-heif.wasm' ) ) { return VipsHeifModule; + } else if ( fileName.endsWith( 'vips-jxl.wasm' ) ) { + return VipsJxlModule; } return fileName; }, @@ -154,6 +158,12 @@ export async function convertImageFormat( saveOptions.effort = 2; } + // JXL default effort of 7 is too slow for interactive use. + // Use 3 for a good balance of speed and compression. + if ( 'image/jxl' === outputType ) { + saveOptions.effort = 3; + } + const outBuffer = image.writeToBuffer( `.${ ext }`, saveOptions ); const result = outBuffer.buffer; @@ -310,6 +320,12 @@ function buildSaveOptions( saveOptions.effort = 2; } + // JXL default effort of 7 is too slow for interactive use. + // Use 3 for a good balance of speed and compression. + if ( 'image/jxl' === type ) { + saveOptions.effort = 3; + } + return saveOptions; } diff --git a/packages/vips/src/utils.ts b/packages/vips/src/utils.ts index 924d28110b597e..d9a546f29dd1a6 100644 --- a/packages/vips/src/utils.ts +++ b/packages/vips/src/utils.ts @@ -8,10 +8,19 @@ */ export function supportsQuality( type: string -): type is 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif' { - return [ 'image/jpeg', 'image/png', 'image/webp', 'image/avif' ].includes( - type - ); +): type is + | 'image/jpeg' + | 'image/png' + | 'image/webp' + | 'image/avif' + | 'image/jxl' { + return [ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/avif', + 'image/jxl', + ].includes( type ); } /**