jxldec
| Language | C (usable from C++) |
|---|---|
| License | MIT, with parts under libjxl's BSD-3-Clause license |
| Repository | github.com/kjk/jxldec |
| Documentation | src/jxl.h, PROGRESS.md (feature matrix) |
| Depends on | nothing |
| Read / write | yes / no (decoder only) |
jxldec decodes JPEG XL images in plain C, with no dependencies. libjxl is C++ and needs highway, brotli and skcms. It’s a port of the Rust decoder jxl-oxide, checked against libjxl’s djxl on a corpus of over 1,200 files. Lossless (Modular) images decode byte for byte the same as libjxl; lossy (VarDCT) images are within one 8-bit step.
It supports animation, alpha, 8 and 16-bit output, embedded ICC profiles, EXIF orientation, progressive frames, patches, splines and noise. It doesn’t encode, doesn’t reconstruct the original JPEG from recompressed JPEG files (it decodes them as images), and is single-threaded. Decoding takes about 1.1 times as long as single-threaded libjxl, using SSE2 and AVX2 kernels picked at runtime.
Installation#
Copy dist/jxl.h and dist/jxl.c from the repository into your project and compile jxl.c with your code. They are the whole library amalgamated into one translation unit.
git clone https://github.com/kjk/jxldec
cp jxldec/dist/jxl.h jxldec/dist/jxl.c myproject/
Basic usage: decode a JPEG XL image to RGBA#
#include "jxl.h"
#include <stdio.h>
#include <stdlib.h>
static uint8_t *read_file(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*len = (size_t)ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *d = malloc(*len);
if (d && fread(d, 1, *len, f) != *len) { free(d); d = NULL; }
fclose(f);
return d;
}
static void on_error(void *user, jxl_severity sev, const char *msg) {
if (sev >= JXLDEC_SEVERITY_ERROR) fprintf(stderr, "jxl: %s\n", msg);
}
int main(void) {
size_t len;
uint8_t *data = read_file("photo.jxl", &len);
if (!data) return 1;
jxl_ctx *ctx = jxl_ctx_new(NULL, NULL, on_error, NULL);
jxl_doc *doc = jxl_doc_open(ctx, data, len); /* data must outlive doc */
if (!doc) return 1;
jxl_image_info info;
jxl_doc_info(doc, &info);
printf("%d x %d, %d bits, alpha: %s, frames: %d\n", info.width, info.height,
info.bits_per_sample, info.alpha_bits ? "yes" : "no", info.num_frames);
jxl_image *img = jxl_frame_render(doc, 0, JXLDEC_FORMAT_RGBA32);
if (img) {
/* img->data: top-down rows of img->stride bytes, 4 bytes per pixel */
jxl_image_destroy(ctx, img);
}
jxl_doc_close(doc);
jxl_ctx_free(ctx);
free(data);
return 0;
}
How to check if a file is JPEG XL#
jxl_signature_check looks at the first bytes; no context needed. JPEG XL files are either a bare codestream (FF 0A) or an ISOBMFF container:
switch (jxl_signature_check(data, len)) {
case JXLDEC_SIG_CODESTREAM: puts("JPEG XL codestream"); break;
case JXLDEC_SIG_CONTAINER: puts("JPEG XL container"); break;
case JXLDEC_SIG_NOT_ENOUGH_BYTES: puts("need more bytes"); break;
default: puts("not JPEG XL");
}
How to get the image size without decoding#
int w, h;
if (jxl_decode_size(ctx, data, len, &w, &h) == 0)
printf("%d x %d\n", w, h);
The size already has the EXIF-style orientation applied, like the decoded image. Call jxl_ctx_set_keep_orientation(ctx, 1) to get the stored orientation instead.
How to decode an animated JPEG XL#
Frames can be requested in any order, but each call decodes frames 0 to N, because a frame can blend with earlier ones. Keep a loop like this one for playback instead of seeking back and forth:
int n = jxl_doc_frame_count(doc);
for (int i = 0; i < n; i++) {
jxl_frame_info fi;
jxl_doc_frame_info(doc, i, &fi);
double ms = 1000.0 * fi.duration_ticks * fi.tps_denominator / fi.tps_numerator;
jxl_image *frame = jxl_frame_render(doc, i, JXLDEC_FORMAT_RGBA32);
if (!frame) break;
printf("frame %d: %.0f ms\n", i, ms);
jxl_image_destroy(ctx, frame);
}
How to decode into a Windows bitmap#
jxl_ctx_set_bgr swaps the channel order during decoding, and jxl_frame_render_into writes into your buffer, so no extra copy or swizzle pass is needed:
jxl_ctx_set_bgr(ctx, 1); /* B,G,R,A like a 32-bit DIB */
jxl_render_info ri;
jxl_frame_render_info(doc, 0, JXLDEC_FORMAT_RGBA32, &ri);
int stride = ri.width * 4;
uint8_t *pixels = malloc((size_t)stride * ri.height);
if (jxl_frame_render_into(doc, 0, JXLDEC_FORMAT_RGBA32, pixels, stride) != 0) {
/* decode failed */
}
Tips#
JXLDEC_FORMAT_NATIVEpicks gray or RGB, with or without alpha, 8 or 16 bits, from the image;JXLDEC_FORMAT_RGBA64keeps 16 bits per channel.- For lossy images that declare a linear transfer function,
jxl_ctx_set_srgb_output(ctx, 1)gives display-ready sRGB. jxl_doc_icc_profilereturns the embedded ICC profile for color management.