# JPEG XL

Canonical URL: https://encode.wiki/codecs/images/jxl/
Content version: ca42e61ede0a24f789d7287a0001244ad22f4abf
Status: current
License: CC BY-SA 4.0
Checksum (SHA-256): 5b84f15d02277e43bd86ab680a416200803962eaab093f2514fe8f3eeccd27b4

JPEG XL (JXL) is the JPEG committee's modern image format (ISO/IEC 18181). It does lossy and lossless coding well, handles HDR, wide gamut, high bit depth, alpha, layers and animation, decodes quickly, and is the only major format that can recompress an existing JPEG losslessly and give you the identical JPEG back. Browser support was the long-standing blocker; as of October 2026 that is changing quickly (see below).

Use it for archives and photo libraries (especially existing JPEG collections), for lossless images, for high-fidelity delivery, and anywhere you control the decoder. For lowest-bitrate web delivery, [AVIF](/codecs/images/avif/) usually still produces smaller files.

## Strengths and weaknesses

**Strengths**

- **Lossless JPEG recompression.** `cjxl photo.jpg photo.jxl` shrinks a typical JPEG by about 20 % and `djxl photo.jxl photo.jpg` restores the original file byte for byte.
- **Strong lossless mode**, usually well below PNG and lossless WebP on photographs, and it handles 16-bit properly.
- **High-fidelity lossy.** It is at its best at medium-to-high quality (distance 0.5–2), with graceful behaviour across a wide quality range.
- **Fast decode, and encoding that scales with effort** from near-JPEG speed to very slow.
- Progressive decoding, up to 32 bits per channel, HDR/wide gamut, many channels, tiled large images (over a billion pixels per side), layers and animation. Royalty-free.

**Weaknesses**

- Browser and application support is newer and patchier than for AVIF or WebP.
- At low bitrates (aggressive web compression) the best AVIF encodes are generally smaller and cleaner.
- Encoder tooling is essentially one implementation, `libjxl`. Its CLI and API still change between releases.

## Support

| Platform | Status as of 2026-10-08                                                                                                                                                              |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Safari   | Still images since Safari 17; animated JXL is not supported.                                                                                                |
| Chrome   | Decoding is on by default from Chrome 155 (stable release scheduled 2026-10-06), using the Rust decoder `jxl-rs`. Chrome 145–154 had it behind `#enable-jxl-image-format`.        |
| Firefox  | Nightly only until Firefox 158, which enables it by default (stable scheduled 2026-10-13), also built on `jxl-rs`.                                                                    |
| Edge, Opera, other Chromium | Follow their Chromium version, so expect support to spread over the following weeks.                                                                           |

Sources: [Safari 17 notes](https://webkit.org/blog/14445/webkit-features-in-safari-17-0/), [Chrome 155 beta](https://developer.chrome.com/blog/chrome-155-beta) and [release notes](https://chromestatus.com/release-notes/155), [MDN's JPEG XL data](https://github.com/mdn/browser-compat-data/blob/main/mediatypes/image/jxl.json), [Firefox 158 beta release notes](https://www.firefox.com/en-US/firefox/158.0beta/releasenotes/) and [Mozilla's intent to ship](https://groups.google.com/a/mozilla.org/g/dev-platform/c/3YMV4MS34KA). Until Chrome 155 and Firefox 158 are widely installed, serve JXL through `<picture>` with an AVIF/WebP/JPEG fallback; [image and audio support](/reference/image-audio-support/) has the markup and a detection snippet.

## Encode with cjxl

`cjxl` and `djxl` come from [libjxl](https://github.com/libjxl/libjxl); most distributions package them.

```sh
cjxl input.png output.jxl -d 1
```

`-d 1` and effort 7 are the defaults for PNG input. The two settings that matter:

- **Distance (`-d`)** is the lossy quality target in roughly just-noticeable-difference units. `-d 1` is the default and is meant to be visually lossless at normal viewing; `-d 0` is mathematically lossless; larger values are smaller and lossier (range 0–25, sensible range 0.5–3). `-q` is a JPEG-like alternative scale (`-q 90` ≈ `-d 1`); use one or the other.
- **Effort (`-e`)** is encoder time versus density, 1–10, default 7. Higher effort mostly shrinks files slightly and makes the achieved quality more accurate; `-e 9`–`10` are worth it for final lossless archives and rarely for lossy delivery.

| Task                              | Command                                                           |
| --------------------------------- | ----------------------------------------------------------------- |
| Lossy, visually lossless          | `cjxl in.png out.jxl -d 1`                                        |
| Smaller web delivery              | `cjxl in.png out.jxl -d 2`                                        |
| Lossless                          | `cjxl in.png out.jxl -d 0 -e 9`                                   |
| Lossless JPEG recompression       | `cjxl in.jpg out.jxl`                                             |
| Fresh lossy encode from a JPEG    | `cjxl in.jpg out.jxl -j 0 -d 1`                                   |
| Progressive loading               | `cjxl in.png out.jxl -d 1 -p`                                     |
| Photo-like grain on smooth images | `cjxl in.png out.jxl -d 1 --photon_noise_iso=800`                 |
| Animation from GIF/APNG           | `cjxl in.gif out.jxl`                                             |
| Decode to PNG                     | `djxl in.jxl out.png`                                             |
| Restore original JPEG             | `djxl in.jxl out.jpg`                                             |

### JPEG input

A JPEG input is recompressed losslessly by default (`--lossless_jpeg=1`), keeping the reconstruction data so `djxl` can emit the original bytes. That is exactly what you want for archiving. If you instead want a new lossy JXL derived from the decoded pixels, pass `-j 0` and a distance. Do not pass `-d 0 -j 0` to a JPEG and expect the recompression: that is a lossless encode of the decoded pixels, and the file ends up several times larger than the JPEG.

Going the other way, `djxl in.jxl out.jpg` restores the original JPEG when the reconstruction data is present. `djxl -j` (`--pixels_to_jpeg`) forces a decode to pixels and a new, lossy JPEG encode, which is not a restore. Stripping metadata with `-x strip=exif` makes reconstruction impossible, and `cjxl` then requires `--allow_jpeg_reconstruction=0`.

### Other options worth knowing

| Option                        | Use                                                                                                                                      |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `-m 0/1`                      | Force VarDCT (0) or modular (1) coding; by default the encoder chooses.                                                                  |
| `-a DISTANCE`                 | Alpha distance; default 0 (lossless alpha).                                                                                               |
| `--keep_invisible=1`          | Keep RGB hidden under fully transparent pixels. It is on for lossless and off for lossy by default.                                       |
| `-p`, `--progressive_dc=0..2` | More progressive/responsive decoding; `--progressive_dc=1` adds an extra low-resolution pass (`2` adds two).              |
| `--faster_decoding=0..4`      | Trade some density for lower decode cost on weak devices.                                                                                 |
| `--photon_noise_iso=N`        | Synthesise film-like noise instead of encoding real grain (100 is a little, 3200 a lot).                                                                |
| `-x color_space=…`            | Tell `cjxl` what colour space an untagged raw input is, or override one (`sRGB`, `DisplayP3`, `Rec2100PQ`, …).                           |
| `--num_threads`               | Worker threads; default is the machine's.                                                                                                 |
| `--container=1`               | Force the ISO container; usually only needed for metadata boxes.                                                                          |

Raw-pixel inputs (PPM, PFM and similar) carry no colour information; supply it with `-x color_space=…` or the output will be interpreted as sRGB. Animation inputs (GIF, APNG) are written as JXL animations with their frame timing; GIF input defaults to lossless (`-d 0`).

## Compare JXL with the alternatives

- **Against JPEG:** clearly smaller at the same quality, and recompressing an existing JPEG saves about 20 % with no loss at all. [jpegli](/codecs/images/jpeg/) narrows the gap for new lossy JPEGs.
- **Against AVIF:** AVIF is generally smaller at web-delivery quality; JXL encodes faster, decodes cheaply, holds up better at high quality and is much better in lossless.
- **Against WebP:** JXL wins in lossy at medium-to-high quality and in lossless, has no 8-bit/4:2:0 limits, and WebP has the support advantage.

For your own comparisons, [SSIMULACRA2](/reference/metrics/ssimulacra2/) and [Butteraugli](/reference/metrics/butteraugli/) are the useful metrics, but libjxl is itself tuned with Butteraugli, so look at the images at 1:1 as well.

## Pitfalls

- `-d` and `-q` are mutually exclusive.
- `-e 10` is far slower than `-e 9` for little gain; on camera-resolution images it is only worth trying for lossless archives.
- Check that the people who need to open the files have a JXL-capable viewer before converting a whole library; browser support is new and desktop and mobile apps vary.
- Re-encoding lossy JXL or lossy JPEG output with a lossy encoder compounds loss; keep sources.

## Other implementations

- [libjxl-tiny](https://github.com/libjxl/libjxl-tiny) is a stripped-down encoder for photographic images meant to guide hardware implementations.
- [Hydrium](https://github.com/Traneptora/hydrium) is a fast, very low-memory streaming encoder in C.
- [zune-jpegxl](https://github.com/etemesi254/zune-image/tree/dev/crates/zune-jpegxl) is a simple Rust encoder with lossless 8- and 16-bit output.
- [jxl-oxide](https://github.com/tirr-c/jxl-oxide) and `jxl-rs` are spec-conforming Rust decoders; `jxl-rs` is what Chrome and Firefox use.

## Related pages

- [JPEG](/codecs/images/jpeg/) is what JXL most directly replaces, and what it can losslessly absorb.
- [AVIF](/codecs/images/avif/) is the AV1-based alternative with better low-bitrate compression.
- [WebP](/codecs/images/webp/) is the older widely supported web format.
- [PNG](/codecs/images/png/) is what lossless JXL most often replaces.

## Full option reference

The table below is the full `cjxl` parameter reference for libjxl 0.12.0. [Reading option tables](/reference/encoder-parameters/) explains the Default column.

## cjxl 0.12.0 complete parameter reference

<details>
<summary>INPUT to --dec-hints</summary>

| Parameter / aliases      | Type or accepted values             | Default                                            | Purpose and constraints                                                                                      |
| ------------------------ | ----------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `INPUT`                  | path                                | required                                           | Source image: JXL, PNG/APNG, GIF, JPEG, PNM/PFM/PAM or PGX. |
| `OUTPUT`                 | path                                | required unless --disable_output                   | Output .jxl path; an existing file is overwritten. |
| `--distance`, `-d`       | float 0–25                          | 0 for JPEG/GIF input; 1 otherwise                  | Quality target in JND units; 0 is lossless, 1 is visually lossless. Excludes --quality. |
| `--quality`, `-q`        | float 0–100                         | unset; input-dependent distance applies            | Libjpeg-like quality mapped to distance; 100 is lossless, 90 is visually lossless. |
| `--effort`, `-e`         | integer 1–10; 11 with expert opt-in | 7                                                  | Encoder effort; higher is slower and usually smaller. 11 needs --allow_expert_options. |
| `--version`, `-V`        | action                              | not requested                                      | Print the encoder and library version, then exit. |
| `--quiet`                | flag                                | false                                              | Suppress progress and size messages. |
| `--verbose`, `-v`        | repeatable flag                     | 0 repetitions                                      | More output; repeat up to four times (-v -v -v -v) to also list advanced options in --help. |
| `--alpha_distance`, `-a` | float 0–25                          | 0                                                  | Distance for the alpha channel, 0–25; 0 keeps alpha lossless. |
| `--progressive`, `-p`    | flag                                | false                                              | Shortcut for progressive_ac, progressive_dc=1, center-first group order, no patches, and Squeeze. |
| `--group_order`          | 0 or 1                              | automatic: normally 0; progressive selects 1       | Codestream group order: 0 scanline, 1 center-first. Center-first suits progressive loading. |
| `--container`            | 0 or 1                              | automatic when metadata/reconstruction requires it | 1 forces the ISOBMFF container; 0 uses it only when metadata or JPEG reconstruction data needs it. |
| `--compress_boxes`       | 0 or 1                              | 1                                                  | Brotli-compress metadata boxes when transcoding JPEG; 0 stores them uncompressed. |
| `--brotli_effort`        | integer 0–11                        | 9                                                  | Brotli effort for metadata boxes, 0–11; higher is smaller but slower. |
| `--modular`, `-m`        | 0 or 1                              | encoder chooses                                    | 1 forces Modular mode (lossless, graphics); 0 forces VarDCT (photos). |
| `--lossless_jpeg`, `-j`  | 0 or 1                              | 1 for JPEG input; disabled for other inputs        | 1 transcodes JPEG input losslessly; set 0 to decode to pixels, required for lossy -d or -q on JPEG input. |
| `--num_threads`          | integer -1 or nonnegative           | -1: machine default                                | Worker threads; 0 disables multithreading and -1 uses all cores. |
| `--photon_noise_iso`     | nonnegative float                   | 0                                                  | Add film-like noise at this ISO (100 light, 3200 heavy); the output differs from the input. |
| `--intensity_target`     | positive float, nits                | 0: choose from color encoding                      | Peak display brightness in nits for HDR content; an explicit 0 is rejected, so omit it for automatic. |
| `--dec-hints`, `-x`      | KEY=VALUE, repeatable               | none                                               | Set or strip metadata, e.g. -x color_space=sRGB, -x icc_pathname=FILE, -x strip=exif; repeatable. |

</details>

<details>
<summary>--allow_jpeg_reconstruction to --jpeg_reconstruction_cfl</summary>

| Parameter / aliases           | Type or accepted values  | Default                        | Purpose and constraints                                                                                                                          |
| ----------------------------- | ------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--allow_jpeg_reconstruction` | 0 or 1                   | 1                              | Store data to restore the original JPEG bytes; 0 gives a smaller file that cannot restore them. |
| `--codestream_level`          | -1, 5 or 10              | -1: encoder chooses            | Limit the codestream to level 5 (smaller limits, broadest decoder support) or 10; -1 chooses automatically. |
| `--buffering`                 | integer -1–3             | -1: encoder chooses            | Input buffering: 0 whole image (best compression), 1–2 streamed; use --output_mode instead of 3. |
| `--faster_decoding`           | integer 0–4              | 0                              | Trade size or quality for faster decoding, 0 (off) to 4. |
| `--premultiply`               | -1, 0 or 1               | -1: preserve input association | Force alpha to straight (0) or premultiplied (1); -1 keeps the input's. |
| `--keep_invisible`            | 0 or 1                   | 1 for lossless; 0 for lossy    | 1 keeps RGB values under fully transparent pixels; 0 lets the encoder alter them for size. |
| `--center_x`                  | -1 or valid x coordinate | -1: image midpoint             | Horizontal center for center-first group order; needs --group_order=1 or --progressive. |
| `--center_y`                  | -1 or valid y coordinate | -1: image midpoint             | Vertical center for center-first group order; needs --group_order=1 or --progressive. |
| `--progressive_ac`            | flag                     | false                          | Split AC coefficients into several passes so the image refines while loading. |
| `--qprogressive_ac`           | flag                     | false                          | Progressive AC passes using shifted quantization instead of coefficient splitting. |
| `--progressive_dc`            | integer -1–2             | -1: encoder chooses            | Extra low-resolution DC passes: 0 off, 1 adds a 64x64 pass, 2 adds 512x512 and 64x64. |
| `--resampling`                | -1, 1, 2, 4 or 8         | -1: input/quality-dependent    | Downsample color channels by 1, 2, 4 or 8; -1 does so only at very low quality. |
| `--ec_resampling`             | -1, 1, 2, 4 or 8         | -1: encoder chooses            | Downsampling factor for extra channels such as alpha; same values as --resampling. |
| `--already_downsampled`       | flag                     | false                          | Input is already downsampled; skip encoder downsampling but tell the decoder to upsample. |
| `--upsampling_mode`           | -1, 0 or 1               | -1: nonseparable               | Decoder upsampling: -1 smooth, 0 nearest neighbor (pixel art), 1 pixel dots at 4x and 8x. |
| `--epf`                       | integer -1–3             | -1: encoder chooses            | Decoder edge-preserving filter strength, 0 (off) to 3; -1 lets the encoder choose. |
| `--gaborish`                  | 0 or 1                   | encoder chooses                | Enable (1) or disable (0) the gaborish smoothing filter; unset lets the encoder choose. |
| `--override_bitdepth`         | unsigned integer         | 0: input depth                 | Bit depth to write; 0 uses the input's depth. |
| `--noise`                     | 0 or 1                   | encoder chooses                | Enable or disable the encoder's adaptive noise synthesis; prefer --photon_noise_iso. |
| `--jpeg_reconstruction_cfl`   | 0 or 1                   | encoder chooses                | Enable or disable chroma-from-luma when transcoding JPEG; unset lets the encoder choose. |

</details>

<details>
<summary>--num_reps to --responsive</summary>

| Parameter / aliases                  | Type or accepted values   | Default                                                 | Purpose and constraints                                                                                 |
| ------------------------------------ | ------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `--num_reps`                         | positive integer          | 1                                                       | Encode this many times, for benchmarking. |
| `--streaming_input`                  | flag                      | false                                                   | Read PPM or PGM input incrementally to reduce memory use. |
| `--streaming_output`                 | flag                      | false                                                   | Write output through a callback as it is produced instead of buffering the whole file. |
| `--output_mode`                      | integer -1–2              | -1: encoder chooses                                     | Output layout: 0 buffered, 1 streamed with seeking, 2 out-of-order jxlp boxes without seeking. |
| `--disable_output`                   | flag                      | false                                                   | Encode without writing a file, for timing. |
| `--dots`                             | 0 or 1                    | encoder chooses                                         | Enable or disable dot detection and coding; unset lets the encoder choose. |
| `--patches`                          | 0 or 1                    | encoder chooses                                         | Enable or disable repeated-patch detection; unset lets the encoder choose. |
| `--frame_indexing`                   | 0/1 index string          | empty: no explicit index selection                      | Bit string marking which frames go in the frame index box, e.g. 1011. |
| `--allow_expert_options`             | flag                      | false                                                   | Permit --effort=11, which is much slower for slightly denser lossless output. |
| `--disable_perceptual_optimizations` | flag                      | false                                                   | Turn off the encoder's perceptual heuristics; listed only at help verbosity 4. |
| `--iterations`, `-I`                 | float -1–100              | -1: encoder chooses                                     | Percent of pixels used to learn modular MA trees; 0 disables the trees. |
| `--modular_colorspace`, `-C`         | integer -1–41             | -1: encoder chooses                                     | Modular color transform: 0 none, 1–41 fixed reversible transform (6 is YCoCg). |
| `--modular_group_size`, `-g`         | integer -1–3              | -1: encoder chooses                                     | Modular group size: 0 128, 1 256, 2 512, 3 1024 pixels per side. |
| `--modular_predictor`, `-P`          | integer -1–15             | -1: normally 14 below effort 10, 15 at effort 10        | Modular predictor 0–15 (5 gradient, 6 weighted, 14 mixes 5 and 6, 15 mixes all). |
| `--modular_nb_prev_channels`, `-E`   | integer -1–11             | -1: encoder chooses                                     | Maximum previous channels used as MA tree properties, 0–11. |
| `--modular_palette_colors`           | integer -1 or nonnegative | -1: encoder chooses                                     | Use a palette when the image has at most this many colors; 0 skips the palette search. |
| `--modular_lossy_palette`            | flag                      | false                                                   | Use lossy delta palette; combine with --modular_palette_colors=0. Ignored in progressive mode. |
| `--pre-compact`, `-X`                | float -1–100              | -1: encoder chooses                                     | Use a global channel palette when sample values are below this percent of the range. |
| `--post-compact`, `-Y`               | float -1–100              | -1: encoder chooses                                     | Use per-group channel palettes when sample values are below this percent of the range. |
| `--responsive`, `-R`                 | -1, 0 or 1                | -1: normally 0 lossless, 1 lossy; progressive selects 1 | Apply the Squeeze transform for resolution-progressive decoding; default 0 lossless, 1 lossy. |

</details>

<details>
<summary>--help and --</summary>

| Parameter / aliases | Type or accepted values | Default       | Purpose and constraints                                   |
| ------------------- | ----------------------- | ------------- | --------------------------------------------------------- |
| `--help`, `-h`      | action                  | not requested | Print help; add -v for advanced options (up to four times). |
| `--`                | separator               | not used      | End of options; later arguments are file paths even if they start with a dash. |

</details>
