All formats

libjxl

LanguageC API (with C++ RAII helpers), implementation in C++
LicenseBSD-3-Clause (plus a patent grant)
Repositorygithub.com/libjxl/libjxl
Documentationlibjxl API reference
Depends onHighway (SIMD), Brotli; optional Little CMS or skcms for color management
Read / writeyes / 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#

Shell
# 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:

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#

C++
#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#

C++
#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:

C++
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:

C++
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#

C
#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:

C++
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#

C++
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#