libjxl
| Language | C API (with C++ RAII helpers), implementation in C++ |
|---|---|
| License | BSD-3-Clause (plus a patent grant) |
| Repository | github.com/libjxl/libjxl |
| Documentation | libjxl API reference |
| Depends on | Highway (SIMD), Brotli; optional Little CMS or skcms for color management |
| Read / write | yes / yes, lossy, lossless, animation, HDR, lossless JPEG recompression and reconstruction |
libjxl is the reference implementation of JPEG XL, developed by Google and Cloudinary together with the JPEG committee. Nearly every program and library that writes JPEG XL uses it: ImageMagick, libvips, GIMP, Krita, FFmpeg, Magick.NET and the bindings for Python, Go and Rust. It also ships the command-line tools cjxl, djxl and jxlinfo.
The API is event-based and streaming. The decoder (jxl/decode.h) reports events such as “basic info is available” or “set an output buffer”, which lets you read just the header, decode progressively, or feed data as it arrives. The encoder (jxl/encode.h) takes pixels or a JPEG file and produces output in chunks. The API is plain C; the *_cxx.h headers add std::unique_ptr wrappers so you don’t need to destroy objects by hand.
Installation#
# Debian / Ubuntu
sudo apt install libjxl-dev libjxl-tools
# Fedora
sudo dnf install libjxl-devel libjxl-utils
# macOS
brew install jpeg-xl
# Windows (vcpkg)
vcpkg install libjxl
libjxl installs pkg-config files. With CMake:
find_package(PkgConfig REQUIRED)
pkg_check_modules(JXL REQUIRED IMPORTED_TARGET libjxl libjxl_threads)
target_link_libraries(myapp PRIVATE PkgConfig::JXL)
Basic usage: decode a JXL file to RGBA pixels#
#include <jxl/decode.h>
#include <jxl/decode_cxx.h>
#include <jxl/resizable_parallel_runner.h>
#include <jxl/resizable_parallel_runner_cxx.h>
#include <cstdio>
#include <fstream>
#include <iterator>
#include <vector>
int main() {
std::ifstream f("in.jxl", std::ios::binary);
std::vector<uint8_t> data((std::istreambuf_iterator<char>(f)), {});
auto runner = JxlResizableParallelRunnerMake(nullptr);
auto dec = JxlDecoderMake(nullptr);
JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE);
JxlDecoderSetParallelRunner(dec.get(), JxlResizableParallelRunner, runner.get());
JxlDecoderSetInput(dec.get(), data.data(), data.size());
JxlDecoderCloseInput(dec.get());
JxlBasicInfo info;
JxlPixelFormat format = {4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0}; // 8-bit RGBA
std::vector<uint8_t> pixels;
for (;;) {
JxlDecoderStatus status = JxlDecoderProcessInput(dec.get());
if (status == JXL_DEC_ERROR || status == JXL_DEC_NEED_MORE_INPUT) {
fprintf(stderr, "decode error\n");
return 1;
} else if (status == JXL_DEC_BASIC_INFO) {
JxlDecoderGetBasicInfo(dec.get(), &info);
printf("%u x %u, %u bits, alpha: %s\n", info.xsize, info.ysize,
info.bits_per_sample, info.alpha_bits ? "yes" : "no");
JxlResizableParallelRunnerSetThreads(
runner.get(), JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize));
} else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) {
size_t size;
JxlDecoderImageOutBufferSize(dec.get(), &format, &size);
pixels.resize(size);
JxlDecoderSetImageOutBuffer(dec.get(), &format, pixels.data(), pixels.size());
} else if (status == JXL_DEC_FULL_IMAGE) {
// pixels holds one frame; animations produce more frames
} else if (status == JXL_DEC_SUCCESS) {
break;
}
}
// pixels: xsize * ysize * 4 bytes, top row first, orientation already applied
return 0;
}
The decoder converts to the pixel format you ask for: change JXL_TYPE_UINT8 to JXL_TYPE_UINT16 or JXL_TYPE_FLOAT for high bit depth and HDR images, and the channel count to 3 for RGB.
How to encode pixels to JPEG XL#
#include <jxl/encode.h>
#include <jxl/encode_cxx.h>
#include <jxl/thread_parallel_runner.h>
#include <jxl/thread_parallel_runner_cxx.h>
#include <vector>
// rgb: width * height * 3 bytes, 8-bit sRGB
bool encode_jxl(const std::vector<uint8_t>& rgb, uint32_t width, uint32_t height,
float quality, std::vector<uint8_t>* out) {
auto enc = JxlEncoderMake(nullptr);
auto runner = JxlThreadParallelRunnerMake(nullptr, JxlThreadParallelRunnerDefaultNumWorkerThreads());
JxlEncoderSetParallelRunner(enc.get(), JxlThreadParallelRunner, runner.get());
JxlBasicInfo info;
JxlEncoderInitBasicInfo(&info);
info.xsize = width;
info.ysize = height;
info.bits_per_sample = 8;
info.num_color_channels = 3;
info.uses_original_profile = JXL_FALSE; // JXL_TRUE for lossless
if (JxlEncoderSetBasicInfo(enc.get(), &info) != JXL_ENC_SUCCESS) return false;
JxlColorEncoding color;
JxlColorEncodingSetToSRGB(&color, JXL_FALSE);
JxlEncoderSetColorEncoding(enc.get(), &color);
JxlEncoderFrameSettings* settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
JxlEncoderSetFrameDistance(settings, JxlEncoderDistanceFromQuality(quality)); // quality 0-100
JxlEncoderFrameSettingsSetOption(settings, JXL_ENC_FRAME_SETTING_EFFORT, 7); // 1-10
JxlPixelFormat format = {3, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0};
if (JxlEncoderAddImageFrame(settings, &format, rgb.data(), rgb.size()) != JXL_ENC_SUCCESS)
return false;
JxlEncoderCloseInput(enc.get());
out->resize(64 * 1024);
uint8_t* next_out = out->data();
size_t avail_out = out->size();
JxlEncoderStatus status;
while ((status = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out)) == JXL_ENC_NEED_MORE_OUTPUT) {
size_t offset = next_out - out->data();
out->resize(out->size() * 2);
next_out = out->data() + offset;
avail_out = out->size() - offset;
}
out->resize(next_out - out->data());
return status == JXL_ENC_SUCCESS;
}
How to encode losslessly#
Set info.uses_original_profile = JXL_TRUE before JxlEncoderSetBasicInfo, then mark the frame lossless instead of setting a distance:
JxlEncoderSetFrameLossless(settings, JXL_TRUE);
How to recompress a JPEG losslessly#
Pass the JPEG file bytes, not decoded pixels. JxlEncoderStoreJPEGMetadata adds the jbrd box so the exact JPEG can be rebuilt later:
auto enc = JxlEncoderMake(nullptr);
JxlEncoderUseContainer(enc.get(), JXL_TRUE);
JxlEncoderStoreJPEGMetadata(enc.get(), JXL_TRUE);
JxlEncoderFrameSettings* settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr);
if (JxlEncoderAddJPEGFrame(settings, jpeg.data(), jpeg.size()) != JXL_ENC_SUCCESS) {
// not a valid JPEG, or uses features JPEG XL can't represent (e.g. arithmetic coding)
}
JxlEncoderCloseInput(enc.get());
// then the same JxlEncoderProcessOutput loop as above
To get the original JPEG back, subscribe to JXL_DEC_JPEG_RECONSTRUCTION when decoding. When that event arrives, give the decoder an output buffer with JxlDecoderSetJPEGBuffer; if it returns JXL_DEC_JPEG_NEED_MORE_OUTPUT, call JxlDecoderReleaseJPEGBuffer (it returns the number of unused bytes), grow the buffer and set it again. The command-line equivalent is djxl in.jxl out.jpg.
How to check if a file is JPEG XL#
#include <jxl/decode.h>
// buf: the first bytes of the file (12 are enough)
JxlSignature sig = JxlSignatureCheck(buf, len);
if (sig == JXL_SIG_CODESTREAM || sig == JXL_SIG_CONTAINER) {
// JPEG XL; JXL_SIG_NOT_ENOUGH_BYTES means "read more", JXL_SIG_INVALID means "not JXL"
}
How to get width and height without decoding#
Subscribe only to JXL_DEC_BASIC_INFO and stop at the first event. Only the header is parsed:
auto dec = JxlDecoderMake(nullptr);
JxlDecoderSubscribeEvents(dec.get(), JXL_DEC_BASIC_INFO);
JxlDecoderSetInput(dec.get(), data.data(), data.size());
JxlDecoderCloseInput(dec.get());
if (JxlDecoderProcessInput(dec.get()) == JXL_DEC_BASIC_INFO) {
JxlBasicInfo info;
JxlDecoderGetBasicInfo(dec.get(), &info);
printf("%u x %u, animation: %d\n", info.xsize, info.ysize, info.have_animation);
}
How to add Exif or XMP metadata#
JxlEncoderUseBoxes(enc.get());
JxlEncoderAddBox(enc.get(), "Exif", exif.data(), exif.size(), JXL_FALSE); // JXL_TRUE = Brotli-compress (brob box)
JxlEncoderAddBox(enc.get(), "xml ", xmp.data(), xmp.size(), JXL_FALSE);
JxlEncoderCloseBoxes(enc.get()); // call after the last box, before the last frame is processed
Exif box data starts with a 4-byte offset to the TIFF header (usually 00 00 00 00).
Tips#
JxlEncoderDistanceFromQualitymaps a JPEG-like 0-100 quality to a distance; distance 1.0 is about quality 90 and is visually lossless.- Animated files report
JXL_DEC_FULL_IMAGEonce per frame. Subscribe toJXL_DEC_FRAMEand callJxlDecoderGetFrameHeaderto get each frame’s duration. - Call
JxlDecoderSetKeepOrientation(dec, JXL_TRUE)to get pixels as stored, without applying the orientation from the header. - For untrusted input, decode in a separate process or check
info.xsize * info.ysizebefore allocating the output buffer.