diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index cbfadcb639..fedce9493c 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -505,6 +505,7 @@ extern const AVInputFormat  ff_image_gif_pipe_demuxer;
 extern const AVInputFormat  ff_image_j2k_pipe_demuxer;
 extern const AVInputFormat  ff_image_jpeg_pipe_demuxer;
 extern const AVInputFormat  ff_image_jpegls_pipe_demuxer;
+extern const AVInputFormat  ff_image_jpegxl_pipe_demuxer;
 extern const AVInputFormat  ff_image_pam_pipe_demuxer;
 extern const AVInputFormat  ff_image_pbm_pipe_demuxer;
 extern const AVInputFormat  ff_image_pcx_pipe_demuxer;
diff --git a/libavformat/img2.c b/libavformat/img2.c
index 4153102c92..d8751d66bf 100644
--- a/libavformat/img2.c
+++ b/libavformat/img2.c
@@ -31,6 +31,7 @@ const IdStrMap ff_img_tags[] = {
     { AV_CODEC_ID_MJPEG,      "mpo"      },
     { AV_CODEC_ID_LJPEG,      "ljpg"     },
     { AV_CODEC_ID_JPEGLS,     "jls"      },
+    { AV_CODEC_ID_JPEGXL,     "jxl"      },
     { AV_CODEC_ID_PNG,        "png"      },
     { AV_CODEC_ID_PNG,        "pns"      },
     { AV_CODEC_ID_PNG,        "mng"      },
diff --git a/libavformat/img2dec.c b/libavformat/img2dec.c
index b535831e1c..5f48e0dde6 100644
--- a/libavformat/img2dec.c
+++ b/libavformat/img2dec.c
@@ -830,6 +830,341 @@ static int jpegls_probe(const AVProbeData *p)
     return 0;
 }
 
+#define jxl_bits(bc) (tbits += (bc), ((p->buf_size + AVPROBE_PADDING_SIZE) * 8 < tbits) ? 0 : (AV_RL64(p->buf + (tbits-(bc))/8) >> ((tbits-(bc)) % 8)) & ~(~(uint64_t)0 << (bc)))
+
+static inline uint32_t ff_jxl_ratio(uint32_t h, uint32_t ratio){
+    switch (ratio){
+    case 1:
+        return h;
+    case 2:
+        return (uint32_t)(((uint64_t)h * 12) / 10);
+    case 3:
+        return (uint32_t)(((uint64_t)h * 4) / 3);
+    case 4:
+        return (uint32_t)(((uint64_t)h * 3) / 2);
+    case 5:
+        return (uint32_t)(((uint64_t)h * 16) / 9);
+    case 6:
+        return (uint32_t)(((uint64_t)h * 5) / 4);
+    case 7:
+        return (uint32_t)(((uint64_t)h * 2) / 1);
+    default:
+        /* width coded separately */
+        return 0;
+    }
+}
+
+static inline uint32_t ff_jxl_u32(const AVProbeData *p, size_t *tbitsp, uint32_t *c, uint32_t *u)
+{
+    size_t tbits;
+    uint32_t bits, ret;
+    tbits = *tbitsp;
+    bits = jxl_bits(2);
+    ret = c[bits];
+    if (u[bits])
+        ret += jxl_bits(u[bits]);
+    *tbitsp = tbits;
+    return ret;
+}
+
+static inline uint64_t ff_jxl_u64(const AVProbeData *p, size_t *tbitsp)
+{
+    size_t tbits;
+    uint64_t bits, ret, shift = 12;
+    tbits = *tbitsp;
+    bits = jxl_bits(2);
+    switch (bits) {
+    case 0:
+        ret = 0;
+        break;
+    case 1:
+        ret = 1 + jxl_bits(4);
+        break;
+    case 2:
+        ret = 17 + jxl_bits(8);
+        break;
+    case 3:
+        ret = jxl_bits(12);
+        while (jxl_bits(1)){
+            if (shift < 60) {
+                ret |= jxl_bits(8) << shift;
+                shift += 8;
+            } else {
+                ret |= jxl_bits(4) << shift;
+                break;
+            }
+        }
+        break;
+    }
+    *tbitsp = tbits;
+    return ret;
+}
+
+static inline uint32_t ff_jxl_bitdepth(const AVProbeData *p, size_t *tbitsp){
+    size_t tbits;
+    uint32_t ret;
+    tbits = *tbitsp;
+    if (jxl_bits(1)) {
+        /* float */
+        ret = ff_jxl_u32(p, &tbits, (uint32_t[]){32, 16, 24, 1}, (uint32_t[]){0, 0, 0, 6});
+        tbits += 4;
+    } else {
+        /* integer */
+        ret = ff_jxl_u32(p, &tbits, (uint32_t[]){8, 10, 12, 1}, (uint32_t[]){0, 0, 0, 6});
+    }
+    *tbitsp = tbits;
+    return ret;
+}
+
+static size_t ff_jxl_size_header(const AVProbeData *p, size_t start_bits, uint32_t *w, uint32_t *h)
+{
+    size_t tbits;
+    uint32_t wr = 0, hr;
+    tbits = start_bits;
+    if (jxl_bits(1)) {
+        /* small size header */
+        hr = (jxl_bits(5) + 1) << 3;
+        /* ratio */
+        wr = ff_jxl_ratio(hr, jxl_bits(3));
+        if (!wr){
+            wr = (jxl_bits(5) + 1) << 3;
+        }
+    } else {
+        /* full size header */
+        hr = 1 + ff_jxl_u32(p, &tbits, (uint32_t[]){0, 0, 0, 0}, (uint32_t[]){9, 13, 18, 30});
+        /* ratio */
+        wr = ff_jxl_ratio(hr, jxl_bits(3));
+        if (!wr){
+            wr = 1 + ff_jxl_u32(p, &tbits, (uint32_t[]){0, 0, 0, 0}, (uint32_t[]){9, 13, 18, 30});
+        }
+    }
+    if (hr > (1 << 18) || wr > (1 << 18))
+        /*
+         * raw JXL codestream files capped at level 5
+         * a violation is a false positive
+         * level 5 mandates that w, h <= 2^18
+         */
+        return 0;
+    if ((hr >> 4) * (wr >> 4) > (1 << 20))
+        /*
+         * (h >> 4) * (w >> 4) avoids uint32_t overflow
+         * level 5 also mandates that w * h <= 2^28
+         */
+        return 0;
+    *h = hr;
+    *w = wr;
+    return tbits - start_bits;
+}
+
+static int jpegxl_probe(const AVProbeData *p)
+{
+    const uint8_t *b = p->buf;
+    uint32_t w, h;
+    int all_default, extra_fields = 0, xyb = 1;
+    size_t tbits = 16, bits, pbits, bonus;
+    uint64_t count;
+
+    /* ISOBMFF-based container */
+    /* 0x4a584c20 == "JXL " */
+    if (AV_RB64(b) == 0x0000000c4a584c20)
+        return AVPROBE_SCORE_EXTENSION + 1;
+    /* Codestreams all start with 0xff0a */
+    if (AV_RB16(b) != 0xff0a)
+        return 0;
+    pbits = ff_jxl_size_header(p, tbits, &w, &h);
+    if (pbits)
+        tbits += pbits;
+    else
+        /* invalid size header */
+        return 0;
+
+    /* all_default */
+    all_default = jxl_bits(1);
+    if (!all_default)
+        extra_fields = jxl_bits(1);
+
+    if (extra_fields) {
+        /* orientation */
+        tbits += 3;
+        /* have intrinstic size */
+        if (jxl_bits(1)) {
+            pbits = ff_jxl_size_header(p, tbits, &w, &h);
+            if (pbits)
+                tbits += pbits;
+            else
+                return 0;
+        }
+        /* have preview */
+        if (jxl_bits(1)) {
+            /* div8 */
+            bits = jxl_bits(1);
+            if (bits)
+                h = ff_jxl_u32(p, &tbits, (uint32_t[]){16, 32, 1, 33}, (uint32_t[]){0, 0, 5, 9});
+            else
+                h = ff_jxl_u32(p, &tbits, (uint32_t[]){1, 65, 321, 1345}, (uint32_t[]){6, 8, 10, 12});
+            if (h > 4096)
+                /* invalid for preview headers */
+                return 0;
+            /* ratio */
+            w = ff_jxl_ratio(h, jxl_bits(3));
+            if (!w){
+                if (bits)
+                    w = ff_jxl_u32(p, &tbits, (uint32_t[]){16, 32, 1, 33}, (uint32_t[]){0, 0, 5, 9});
+                else
+                    w = ff_jxl_u32(p, &tbits, (uint32_t[]){1, 65, 321, 1345}, (uint32_t[]){6, 8, 10, 12});
+            }
+            if (w > 4096)
+                /* invalid for preview headers */
+                return 0;
+        }
+        /* have animation */
+        if (jxl_bits(1)) {
+            /* timebase */
+            ff_jxl_u32(p, &tbits, (uint32_t[]){100, 1000, 1, 1}, (uint32_t[]){0, 0, 10, 30});
+            ff_jxl_u32(p, &tbits, (uint32_t[]){1, 1001, 1, 1}, (uint32_t[]){0, 0, 8, 10});
+            /* loopcount */
+            ff_jxl_u32(p, &tbits, (uint32_t[]){0, 0, 0, 0}, (uint32_t[]){0, 3, 16, 32});
+            /* PTS present */
+            tbits += 1;
+        }
+    }
+
+    if (!all_default) {
+        /* bit depth */
+        ff_jxl_bitdepth(p, &tbits);
+        /* modular 16-bit */
+        tbits += 1;
+        /* extra channel count */
+        count = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 1}, (uint32_t[]){0, 0, 4, 12});
+        for (int i = 0; i < count; i++){
+            if (!jxl_bits(1)){
+                /* type */
+                bits = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6});
+                if (bits > 63)
+                    /* enum types cannot be 64+ */
+                    return 0;
+                /* bit depth */
+                ff_jxl_bitdepth(p, &tbits);
+                /* dimension shift */
+                ff_jxl_u32(p, &tbits, (uint32_t[]){0, 3, 4, 1}, (uint32_t[]){0, 0, 0, 3});
+                /* name len */
+                pbits = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 0, 16, 48}, (uint32_t[]){0, 4, 5, 10});
+                /* name, UTF-8 (not parsing it) */
+                tbits += 8 * pbits;
+                /* alpha channel */
+                if (bits == 1)
+                    /* alpha premultiplied */
+                    tbits += 1;
+                /* spot color channel */
+                if (bits == 2)
+                    /* RGB+S */
+                    tbits += 16 * 4;
+                /* color filter array channel */
+                if (bits == 5)
+                    ff_jxl_u32(p, &tbits, (uint32_t[]){1, 0, 3, 19}, (uint32_t[]){0, 2, 4, 8});
+            }
+        }
+        xyb = jxl_bits(1);
+        /* color encoding */
+        if (!jxl_bits(1)) {
+            /* icc profile */
+            bonus = jxl_bits(1);
+            bits = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6});
+            if (bits > 63)
+                return 0;
+            if (!bonus){
+                /* bits == 2 -> XYB color space */
+                if (bits != 2)
+                    /* white point */
+                    pbits = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6});
+                else
+                    pbits = 1;
+                if (pbits > 63)
+                    return 0;
+                /* custom white point */
+                if (pbits == 2) {
+                    for (int i = 0; i < 2; i++)
+                        ff_jxl_u32(p, &tbits, (uint32_t[]){0, 524288, 1048576, 2097152}, (uint32_t[]){19, 19, 20, 21});
+                }
+                /* bits == 1 -> grayscale */
+                if (bits != 2 && bits != 1)
+                    /* primaries */
+                    pbits = ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6});
+                if (pbits > 63)
+                    return 0;
+                /* custom primaries */
+                if (pbits == 2){
+                    for (int i = 0; i < 6; i++)
+                        ff_jxl_u32(p, &tbits, (uint32_t[]){0, 524288, 1048576, 2097152}, (uint32_t[]){19, 19, 20, 21});
+                }
+                /* gamma is present */
+                if (jxl_bits(1)){
+                    /* gamma */
+                    tbits += 24;
+                } else {
+                    /* transfer function */
+                    if (ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6}) > 63)
+                        return 0;
+                }
+                /* rendering intent */
+                if (ff_jxl_u32(p, &tbits, (uint32_t[]){0, 1, 2, 18}, (uint32_t[]){0, 0, 4, 6}) > 63)
+                    return 0;
+            }
+        }
+    }
+
+    /* tone mapping */
+    if (extra_fields){
+        /* everything default */
+        if (!jxl_bits(1)){
+            /* three 16-bit fields and a bool */
+            tbits += 16 * 3 + 1;
+        }
+    }
+
+    /* extensions */
+    if (!all_default){
+        count = ff_jxl_u64(p, &tbits);
+        while (count){
+            if (count & 1)
+                ff_jxl_u64(p, &tbits);
+            count >>= 1;
+        }
+    }
+
+    /* default transform */
+    bonus = jxl_bits(1);
+    if (!bonus && xyb){
+        /* opsin inverse matrix */
+        bits = jxl_bits(1);
+        if (!bits){
+            /* 16 fields, 16 bits each */
+            tbits += 16 * 16;
+        }
+        bits = jxl_bits(3);
+    } else {
+        bits = 0;
+    }
+    if (bits & 1)
+        tbits += 16 * 15;
+    if (bits & 2)
+        tbits += 16 * 55;
+    if (bits & 4)
+        tbits += 16 * 210;
+    bits = tbits % 8;
+    if (bits)
+        bits = jxl_bits(8 - bits);
+    if (bits)
+        /* header is zero padded to the byte */
+        return 0;
+
+    if (p->buf_size < (tbits - 1) / 8 + 2)
+        /* file too small */
+        return 0;
+    return AVPROBE_SCORE_EXTENSION + 1;
+}
+#undef jxl_bits
+
 static int pcx_probe(const AVProbeData *p)
 {
     const uint8_t *b = p->buf;
@@ -1149,6 +1484,7 @@ IMAGEAUTO_DEMUXER(gif,     AV_CODEC_ID_GIF)
 IMAGEAUTO_DEMUXER(j2k,     AV_CODEC_ID_JPEG2000)
 IMAGEAUTO_DEMUXER(jpeg,    AV_CODEC_ID_MJPEG)
 IMAGEAUTO_DEMUXER(jpegls,  AV_CODEC_ID_JPEGLS)
+IMAGEAUTO_DEMUXER(jpegxl,  AV_CODEC_ID_JPEGXL)
 IMAGEAUTO_DEMUXER(pam,     AV_CODEC_ID_PAM)
 IMAGEAUTO_DEMUXER(pbm,     AV_CODEC_ID_PBM)
 IMAGEAUTO_DEMUXER(pcx,     AV_CODEC_ID_PCX)
diff --git a/libavformat/img2enc.c b/libavformat/img2enc.c
index 62202de9f4..72e64da984 100644
--- a/libavformat/img2enc.c
+++ b/libavformat/img2enc.c
@@ -259,9 +259,9 @@ static const AVClass img2mux_class = {
 const AVOutputFormat ff_image2_muxer = {
     .name           = "image2",
     .long_name      = NULL_IF_CONFIG_SMALL("image2 sequence"),
-    .extensions     = "bmp,dpx,exr,jls,jpeg,jpg,ljpg,pam,pbm,pcx,pfm,pgm,pgmyuv,png,"
-                      "ppm,sgi,tga,tif,tiff,jp2,j2c,j2k,xwd,sun,ras,rs,im1,im8,im24,"
-                      "sunras,xbm,xface,pix,y",
+    .extensions     = "bmp,dpx,exr,jls,jpeg,jpg,jxl,ljpg,pam,pbm,pcx,pfm,pgm,pgmyuv,"
+                      "png,ppm,sgi,tga,tif,tiff,jp2,j2c,j2k,xwd,sun,ras,rs,im1,im8,"
+                      "im24,sunras,xbm,xface,pix,y",
     .priv_data_size = sizeof(VideoMuxData),
     .video_codec    = AV_CODEC_ID_MJPEG,
     .write_header   = write_header,
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 57c67e3aac..3a23f5dad8 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -7437,6 +7437,7 @@ static int mov_probe(const AVProbeData *p)
             if (tag == MKTAG('f','t','y','p') &&
                        (   AV_RL32(p->buf + offset + 8) == MKTAG('j','p','2',' ')
                         || AV_RL32(p->buf + offset + 8) == MKTAG('j','p','x',' ')
+                        || AV_RL32(p->buf + offset + 8) == MKTAG('j','x','l',' ')
                     )) {
                 score = FFMAX(score, 5);
             } else {
