Skip to content

Add JPEG XL (JXL) encoder#1506

Merged
olivervogel merged 2 commits into
Intervention:developfrom
nlemoine:feature/jxl-encoder
Jul 9, 2026
Merged

Add JPEG XL (JXL) encoder#1506
olivervogel merged 2 commits into
Intervention:developfrom
nlemoine:feature/jxl-encoder

Conversation

@nlemoine

@nlemoine nlemoine commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This adds JPEG XL (JXL) as an encodable format.

It works like the HEIC encoder. Encoding goes through Imagick, and I wired JXL into the Format, MediaType and FileExtension enums. There's no JXL in GD, so the GD driver reports it as unsupported and throws NotSupportedException, same as HEIC today.

The encoder takes quality, strip and a lossless flag. For lossless I set the compression quality to 100, that's what ImageMagick's coder uses to switch libjxl to lossless mode (distance 0). See coders/jxl.c.

Putting image/jxl in the MediaType enum also lets the Imagick binary decoder read and round-trip .jxl input, since it gets the origin from getImageMimeType(). So reading works too, on any build that has libjxl.

I also added image/x-jxl next to image/jxl. It's not just an input alias. On one CI runner libmagic detected an encoded JXL blob as image/x-jxl while the others said image/jxl, so the test accepts both.

CI was the tricky part. libjxl-dev isn't packaged for ubuntu-22.04, it's only there from 24.04 on, so I moved the workflow to ubuntu-24.04. That brought out a second problem: 24.04 ships libheif 1.17, which moved its encoders into separate plugin packages, so libheif-dev on its own can't encode AVIF or HEIC anymore and those existing tests started failing with empty output. Installing the x265 and aomenc libheif plugins (plus the matching decoders) brings AVIF and HEIC back. I also bumped the ImageMagick cache key so the cached build gets rebuilt against libjxl.

One more thing on the tests. ImageMagick 6.9.13 doesn't build the JXL coder even when libjxl is there, only 7.1.2 does (you can see it in queryFormats). So the JXL encoder test and the Imagick JXL supports check skip when Imagick::queryFormats('JXL') is empty. They run on the 7.1.2 cells and skip on 6.9.13. I used queryFormats to stay consistent with how Driver::supports() already decides what's supported.

All six matrix cells are green. JXL runs for real on the three 7.1.2 cells (encode and lossless), and skips on the 6.9.13 cells.

Two things I'm not sure about, tell me what you prefer:

  • image/x-jxl in the MediaType enum. I can drop it and keep only image/jxl, but then the media type check in the test has to tolerate whatever libmagic returns on a given runner.
  • Moving CI to ubuntu-24.04. It was the simplest way to get libjxl, but it dragged in the libheif plugin change. If you'd rather stay on 22.04 I can try building libjxl another way, or just keep JXL out of CI.

Browser support is still niche for now. ~14-16%, Safari-only by default. But the trend just turned: Chrome 145 (Feb 2026) re-added a Rust JXL decoder behind a flag, Firefox 152 (June 2026) put it in the release channel behind a Labs flag, and Chrome is expected to flip it on by default in H2 2026. Meaning it could jump to ~85-90% almost overnight. Having it now seems like the good time.

JXL is wired through the Format, MediaType and FileExtension enums and
encoded with Imagick, mirroring the HEIC encoder. GD has no JXL support,
so the GD driver reports it as unsupported and throws NotSupportedException.

The encoder exposes quality, strip and a lossless flag. Lossless maps to a
compression quality of 100, which is how ImageMagick's coder switches libjxl
to lossless (distance 0). Adding image/jxl (and image/x-jxl, which libmagic
reports for JXL blobs on some builds) to the media type enum also lets the
Imagick binary decoder read and round-trip .jxl input.

CI runs on ubuntu-24.04 because libjxl-dev is not packaged for 22.04. That
also needs the libheif encoder plugins (x265, aomenc), since 24.04's libheif
moved its encoders out of libheif-dev and AVIF/HEIC would otherwise fail to
encode. ImageMagick 6.9.13 does not build the JXL coder even with libjxl, so
the JXL tests skip when Imagick::queryFormats('JXL') is empty and run on the
7.1.2 cells.
@olivervogel

Copy link
Copy Markdown
Member

Thanks. That's a really nice addition.

@nlemoine

nlemoine commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@olivervogel I'll add the encoder to vips once it lands (vips needs the core Enums).

@deluxetom

Copy link
Copy Markdown
Contributor

@nlemoine thanks a lot! this is something I was looking forward to

Comment thread src/Encoders/JxlEncoder.php Outdated
JXL has no independent lossless setting in ImageMagick. Lossless is just a
compression quality of 100 (see coders/jxl.c), so a separate lossless flag
was redundant and wrongly implied quality and lossless could be set
independently. Dropping it also makes JxlEncoder match AvifEncoder and
HeicEncoder.
@olivervogel
olivervogel merged commit 830907f into Intervention:develop Jul 9, 2026
6 checks passed
@nlemoine
nlemoine deleted the feature/jxl-encoder branch July 9, 2026 13:14
@nlemoine

nlemoine commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Thank you both @olivervogel & @deluxetom !

@nlemoine

Copy link
Copy Markdown
Contributor Author

@olivervogel The JXL vips encoder is ready, let me know when it's released.

@olivervogel

Copy link
Copy Markdown
Member

Just released 4.2.0.

@nlemoine

Copy link
Copy Markdown
Contributor Author

Thanks! Intervention/image-driver-vips#115

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