diff --git a/packages/upload-media/CHANGELOG.md b/packages/upload-media/CHANGELOG.md index 2a61f113196cdf..0d4b0ecc4721af 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. + ## 0.30.0 (2026-04-29) ### Enhancement 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 3befa57f685e27..5fccfd338df3de 100644 --- a/packages/upload-media/src/store/private-actions.ts +++ b/packages/upload-media/src/store/private-actions.ts @@ -565,7 +565,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 07c4bb2029deeb..4c8a3be8f126c7 100644 --- a/packages/upload-media/src/store/types.ts +++ b/packages/upload-media/src/store/types.ts @@ -333,4 +333,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 7af30a8e39e304..135ae806920070 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.5.0 (2026-04-29) ## 1.4.0 (2026-04-15) 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 ); } /**