Leo Izen: > This commit adds support to libavformat for muxing > and demuxing Jpeg XL images as image2 streams. > --- > MAINTAINERS | 1 + > libavformat/Makefile | 1 + > libavformat/allformats.c | 1 + > libavformat/img2.c | 1 + > libavformat/img2dec.c | 18 ++ > libavformat/img2enc.c | 6 +- > libavformat/jpegxl_probe.c | 393 +++++++++++++++++++++++++++++++++++++ > libavformat/jpegxl_probe.h | 32 +++ > libavformat/mov.c | 1 + > 9 files changed, 451 insertions(+), 3 deletions(-) > create mode 100644 libavformat/jpegxl_probe.c > create mode 100644 libavformat/jpegxl_probe.h > > diff --git a/MAINTAINERS b/MAINTAINERS > index faea84ebf1..46723972dc 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -439,6 +439,7 @@ Muxers/Demuxers: > ipmovie.c Mike Melanson > ircam* Paul B Mahol > iss.c Stefan Gehrer > + jpegxl_probe.* Leo Izen > jvdec.c Peter Ross > kvag.c Zane van Iperen > libmodplug.c Clément Bœsch > diff --git a/libavformat/Makefile b/libavformat/Makefile > index d7182d6bd8..beecdf5a66 100644 > --- a/libavformat/Makefile > +++ b/libavformat/Makefile > @@ -272,6 +272,7 @@ OBJS-$(CONFIG_IMAGE_GIF_PIPE_DEMUXER) += img2dec.o > img2.o > OBJS-$(CONFIG_IMAGE_J2K_PIPE_DEMUXER) += img2dec.o img2.o > OBJS-$(CONFIG_IMAGE_JPEG_PIPE_DEMUXER) += img2dec.o img2.o > OBJS-$(CONFIG_IMAGE_JPEGLS_PIPE_DEMUXER) += img2dec.o img2.o > +OBJS-$(CONFIG_IMAGE_JPEGXL_PIPE_DEMUXER) += img2dec.o img2.o jpegxl_probe.o > OBJS-$(CONFIG_IMAGE_PAM_PIPE_DEMUXER) += img2dec.o img2.o > OBJS-$(CONFIG_IMAGE_PBM_PIPE_DEMUXER) += img2dec.o img2.o > OBJS-$(CONFIG_IMAGE_PCX_PIPE_DEMUXER) += img2dec.o img2.o > diff --git a/libavformat/allformats.c b/libavformat/allformats.c > index 7c1d0ac38f..63876c468f 100644 > --- a/libavformat/allformats.c > +++ b/libavformat/allformats.c > @@ -510,6 +510,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 fe2ca7bfff..566ef873ca 100644 > --- a/libavformat/img2.c > +++ b/libavformat/img2.c > @@ -88,6 +88,7 @@ const IdStrMap ff_img_tags[] = { > { AV_CODEC_ID_GEM, "ximg" }, > { AV_CODEC_ID_GEM, "timg" }, > { AV_CODEC_ID_VBN, "vbn" }, > + { AV_CODEC_ID_JPEGXL, "jxl" }, > { AV_CODEC_ID_NONE, NULL } > }; > > diff --git a/libavformat/img2dec.c b/libavformat/img2dec.c > index 551b9d508e..627bb67212 100644 > --- a/libavformat/img2dec.c > +++ b/libavformat/img2dec.c > @@ -36,6 +36,7 @@ > #include "avio_internal.h" > #include "internal.h" > #include "img2.h" > +#include "jpegxl_probe.h" > #include "libavcodec/mjpeg.h" > #include "libavcodec/vbn.h" > #include "libavcodec/xwd.h" > @@ -837,6 +838,22 @@ static int jpegls_probe(const AVProbeData *p) > return 0; > } > > +static int jpegxl_probe(const AVProbeData *p) > +{ > + const uint8_t *b = p->buf; > + > + /* ISOBMFF-based container */ > + /* 0x4a584c20 == "JXL " */ > + if (AV_RL64(b) == FF_JPEGXL_CONTAINER_SIGNATURE_LE) > + return AVPROBE_SCORE_EXTENSION + 1; > + /* Raw codestreams all start with 0xff0a */ > + if (AV_RL16(b) != FF_JPEGXL_CODESTREAM_SIGNATURE_LE) > + return 0; > + if (ff_jpegxl_verify_codestream_header(p->buf, p->buf_size) >= 0)
This will give a linking failure if the image_jpegxl_pipe_demuxer is disabled. > + return AVPROBE_SCORE_MAX - 2; > + return 0; > +} > + > static int pcx_probe(const AVProbeData *p) > { > const uint8_t *b = p->buf; > @@ -1176,6 +1193,7 @@ IMAGEAUTO_DEMUXER(gif, GIF) > IMAGEAUTO_DEMUXER_EXT(j2k, JPEG2000, J2K) > IMAGEAUTO_DEMUXER_EXT(jpeg, MJPEG, JPEG) > IMAGEAUTO_DEMUXER(jpegls, JPEGLS) > +IMAGEAUTO_DEMUXER(jpegxl, JPEGXL) > IMAGEAUTO_DEMUXER(pam, PAM) > IMAGEAUTO_DEMUXER(pbm, PBM) > IMAGEAUTO_DEMUXER(pcx, PCX) > diff --git a/libavformat/img2enc.c b/libavformat/img2enc.c > index ae351963d9..5ed97bb833 100644 > --- a/libavformat/img2enc.c > +++ b/libavformat/img2enc.c > @@ -263,9 +263,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,vbn,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,vbn,xbm,xface,pix,y", > .priv_data_size = sizeof(VideoMuxData), > .video_codec = AV_CODEC_ID_MJPEG, > .write_header = write_header, > diff --git a/libavformat/jpegxl_probe.c b/libavformat/jpegxl_probe.c > new file mode 100644 > index 0000000000..d3d3822fee > --- /dev/null > +++ b/libavformat/jpegxl_probe.c > @@ -0,0 +1,393 @@ > +/* > + * Jpeg XL header verification > + * Copyright (c) 2022 Leo Izen <leo.i...@gmail.com> > + * > + * This file is part of FFmpeg. > + * > + * FFmpeg is free software; you can redistribute it and/or > + * modify it under the terms of the GNU Lesser General Public > + * License as published by the Free Software Foundation; either > + * version 2.1 of the License, or (at your option) any later version. > + * > + * FFmpeg is distributed in the hope that it will be useful, > + * but WITHOUT ANY WARRANTY; without even the implied warranty of > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU > + * Lesser General Public License for more details. > + * > + * You should have received a copy of the GNU Lesser General Public > + * License along with FFmpeg; if not, write to the Free Software > + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 > USA > + */ > + > +#include "jpegxl_probe.h" > + > +#define BITSTREAM_READER_LE > +#include "libavcodec/get_bits.h" > + > +enum JpegXLExtraChannelType { > + FF_JPEGXL_CT_ALPHA = 0, > + FF_JPEGXL_CT_DEPTH, > + FF_JPEGXL_CT_SPOT_COLOR, > + FF_JPEGXL_CT_SELECTION_MASK, > + FF_JPEGXL_CT_BLACK, > + FF_JPEGXL_CT_CFA, > + FF_JPEGXL_CT_THERMAL, > + FF_JPEGXL_CT_NON_OPTIONAL = 15, > + FF_JPEGXL_CT_OPTIONAL > +}; > + > +enum JpegXLColorSpace { > + FF_JPEGXL_CS_RGB = 0, > + FF_JPEGXL_CS_GRAY, > + FF_JPEGXL_CS_XYB, > + FF_JPEGXL_CS_UNKNOWN > +}; > + > +enum JpegXLWhitePoint { > + FF_JPEGXL_WP_D65 = 1, > + FF_JPEGXL_WP_CUSTOM, > + FF_JPEGXL_WP_E = 10, > + FF_JPEGXL_WP_DCI = 11 > +}; > + > +enum JpegXLPrimaries { > + FF_JPEGXL_PR_SRGB = 1, > + FF_JPEGXL_PR_CUSTOM, > + FF_JPEGXL_PR_2100 = 9, > + FF_JPEGXL_PR_P3 = 11, > +}; > + > +#define jxl_bits(n) get_bits_long(gb, (n)) > +#define jxl_bits_skip(n) skip_bits_long(gb, (n)) > +#define jxl_u32(c0, c1, c2, c3, u0, u1, u2, u3) jpegxl_u32(gb, \ > + (const uint32_t[]){c0, c1, c2, c3}, (const uint32_t[]){u0, u1, u2, u3}) > +#define jxl_u64() jpegxl_u64(gb) > +#define jxl_enum() jxl_u32(0, 1, 2, 18, 0, 0, 4, 6) > + > +/* read a U32(c_i + u(u_i)) */ > +static uint32_t jpegxl_u32(GetBitContext *gb, > + const uint32_t constants[4], const uint32_t > ubits[4]) > +{ > + uint32_t ret, choice = jxl_bits(2); > + > + ret = constants[choice]; > + if (ubits[choice]) > + ret += jxl_bits(ubits[choice]); > + > + return ret; > +} > + > +/* read a U64() */ > +static uint64_t jpegxl_u64(GetBitContext *gb) > +{ > + uint64_t shift = 12, ret; > + > + switch (jxl_bits(2)) { > + 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; > + } > + > + return ret; > +} > + > +static uint32_t jpegxl_width_from_ratio(uint32_t height, int ratio) > +{ > + uint64_t height64 = height; > + switch (ratio) { > + case 1: > + return height; > + case 2: > + return (uint32_t)((height64 * 12) / 10); > + case 3: > + return (uint32_t)((height64 * 4) / 3); > + case 4: > + return (uint32_t)((height64 * 3) / 2); > + case 5: > + return (uint32_t)((height64 * 16) / 9); > + case 6: > + return (uint32_t)((height64 * 5) / 4); > + case 7: > + return (uint32_t)(height64 * 2); > + default: > + break; > + } > + > + return 0; /* manual width */ > +} > + > +/** > + * validate a Jpeg XL Size Header > + * @return >= 0 upon valid size, < 0 upon invalid size found > + */ > +static int jpegxl_read_size_header(GetBitContext *gb) > +{ > + uint32_t width, height; > + > + if (jxl_bits(1)) { > + /* small size header */ > + height = (jxl_bits(5) + 1) << 3; > + width = jpegxl_width_from_ratio(height, jxl_bits(3)); > + if (!width) > + width = (jxl_bits(5) + 1) << 3; > + } else { > + /* large size header */ > + height = 1 + jxl_u32(0, 0, 0, 0, 9, 13, 18, 30); > + width = jpegxl_width_from_ratio(height, jxl_bits(3)); > + if (!width) > + width = 1 + jxl_u32(0, 0, 0, 0, 9, 13, 18, 30); > + } > + if (width > (1 << 18) || height > (1 << 18) > + || (width >> 4) * (height >> 4) > (1 << 20)) > + return -1; > + > + return 0; > +} > + > +/** > + * validate a Jpeg XL Preview Header > + * @return >= 0 upon valid size, < 0 upon invalid size found > + */ > +static int jpegxl_read_preview_header(GetBitContext *gb) > +{ > + uint32_t width, height; > + > + if (jxl_bits(1)) { > + /* coded height and width divided by eight */ > + height = jxl_u32(16, 32, 1, 33, 0, 0, 5, 9) << 3; > + width = jpegxl_width_from_ratio(height, jxl_bits(3)); > + if (!width) > + width = jxl_u32(16, 32, 1, 33, 0, 0, 5, 9) << 3; > + } else { > + /* full height and width coded */ > + height = jxl_u32(1, 65, 321, 1345, 6, 8, 10, 12); > + width = jpegxl_width_from_ratio(height, jxl_bits(3)); > + if (!width) > + width = jxl_u32(1, 65, 321, 1345, 6, 8, 10, 12); > + } > + if (width > 4096 || height > 4096) > + return -1; > + > + return 0; > +} > + > +/** > + * skip a Jpeg XL BitDepth Header. These cannot be invalid. > + */ > +static void jpegxl_skip_bit_depth(GetBitContext *gb) > +{ > + if (jxl_bits(1)) { > + /* float samples */ > + jxl_u32(32, 16, 24, 1, 0, 0, 0, 6); /* mantissa */ > + jxl_bits_skip(4); /* exponent */ > + } else { > + /* integer samples */ > + jxl_u32(8, 10, 12, 1, 0, 0, 0, 6); > + } > +} > + > +/** > + * validate a Jpeg XL Preview Header > + * @return >= 0 upon valid, < 0 upon invalid > + */ > +static int jpegxl_read_extra_channel_info(GetBitContext *gb) > +{ > + int all_default = jxl_bits(1); > + uint32_t type, name_len = 0; > + > + if (!all_default) { > + type = jxl_enum(); > + if (type > 63) > + return -1; /* enum types cannot be 64+ */ > + if (type == FF_JPEGXL_CT_BLACK) > + return -1; > + jpegxl_skip_bit_depth(gb); > + jxl_u32(0, 3, 4, 1, 0, 0, 0, 3); /* dim-shift */ > + /* max of name_len is 1071 = 48 + 2^10 - 1 */ > + name_len = jxl_u32(0, 0, 16, 48, 0, 4, 5, 10); > + } else { > + type = FF_JPEGXL_CT_ALPHA; > + } > + > + /* skip over the name */ > + jxl_bits_skip(8 * name_len); > + > + if (!all_default && type == FF_JPEGXL_CT_ALPHA) > + jxl_bits_skip(1); > + > + if (type == FF_JPEGXL_CT_SPOT_COLOR) > + jxl_bits_skip(16 * 4); > + > + if (type == FF_JPEGXL_CT_CFA) > + jxl_u32(1, 0, 3, 19, 0, 2, 4, 8); > + > + return 0; > +} > + > +/* verify that a codestream header is valid */ > +int ff_jpegxl_verify_codestream_header(const uint8_t *buf, int buflen) > +{ > + GetBitContext gbi, *gb = &gbi; > + int all_default, extra_fields = 0; > + int xyb_encoded = 1, have_icc_profile = 0; > + uint32_t num_extra_channels; > + uint64_t extensions; > + > + init_get_bits8(gb, buf, buflen); > + > + if (jxl_bits(16) != FF_JPEGXL_CODESTREAM_SIGNATURE_LE) > + return -1; > + > + if (jpegxl_read_size_header(gb) < 0) > + return -1; > + > + all_default = jxl_bits(1); > + if (!all_default) > + extra_fields = jxl_bits(1); > + > + if (extra_fields) { > + jxl_bits_skip(3); /* orientation */ > + > + /* > + * intrinstic size > + * any size header here is valid, but as it > + * is variable length we have to read it > + */ > + if (jxl_bits(1)) > + jpegxl_read_size_header(gb); > + > + /* preview header */ > + if (jxl_bits(1)) { > + if (jpegxl_read_preview_header(gb) < 0) > + return -1; > + } > + > + /* animation header */ > + if (jxl_bits(1)) { > + jxl_u32(100, 1000, 1, 1, 0, 0, 10, 30); > + jxl_u32(1, 1001, 1, 1, 0, 0, 8, 10); > + jxl_u32(0, 0, 0, 0, 0, 3, 16, 32); > + jxl_bits_skip(1); > + } > + } > + > + if (!all_default) { > + jpegxl_skip_bit_depth(gb); > + > + /* modular_16bit_buffers must equal 1 */ > + if (!jxl_bits(1)) > + return -1; > + > + num_extra_channels = jxl_u32(0, 1, 2, 1, 0, 0, 4, 12); > + if (num_extra_channels > 4) > + return -1; > + for (uint32_t i = 0; i < num_extra_channels; i++) { > + if (jpegxl_read_extra_channel_info(gb) < 0) > + return -1; > + } > + > + xyb_encoded = jxl_bits(1); > + > + /* color encoding bundle */ > + if (!jxl_bits(1)) { > + uint32_t color_space; > + have_icc_profile = jxl_bits(1); > + color_space = jxl_enum(); > + if (color_space > 63) > + return -1; > + > + if (!have_icc_profile) { > + if (color_space != FF_JPEGXL_CS_XYB) { > + uint32_t white_point = jxl_enum(); > + if (white_point > 63) > + return -1; > + if (white_point == FF_JPEGXL_WP_CUSTOM) { > + /* ux and uy values */ > + jxl_u32(0, 524288, 1048576, 2097152, 19, 19, 20, 21); > + jxl_u32(0, 524288, 1048576, 2097152, 19, 19, 20, 21); > + } > + if (color_space != FF_JPEGXL_CS_GRAY) { > + /* primaries */ > + uint32_t primaries = jxl_enum(); > + if (primaries > 63) > + return -1; > + if (primaries == FF_JPEGXL_PR_CUSTOM) { > + /* ux/uy values for r,g,b */ > + for (int i = 0; i < 6; i++) > + jxl_u32(0, 524288, 1048576, 2097152, 19, 19, > 20, 21); > + } > + } > + } > + > + /* transfer characteristics */ > + if (jxl_bits(1)) { > + /* gamma */ > + jxl_bits_skip(24); > + } else { > + /* transfer function */ > + if (jxl_enum() > 63) > + return -1; > + } > + > + /* rendering intent */ > + if (jxl_enum() > 63) > + return -1; > + } > + } > + > + /* tone mapping bundle */ > + if (extra_fields && !jxl_bits(1)) > + jxl_bits_skip(16 + 16 + 1 + 16); > + > + extensions = jxl_u64(); > + if (extensions) { > + for (int i = 0; i < 64; i++) { > + if (extensions & (UINT64_C(1) << i)) > + jxl_u64(); > + } > + } > + } > + > + /* default transform */ > + if (!jxl_bits(1)) { > + /* opsin inverse matrix */ > + if (xyb_encoded && !jxl_bits(1)) > + jxl_bits_skip(16 * 16); > + /* cw_mask and default weights */ > + if (jxl_bits(1)) > + jxl_bits_skip(16 * 15); > + if (jxl_bits(1)) > + jxl_bits_skip(16 * 55); > + if (jxl_bits(1)) > + jxl_bits_skip(16 * 210); > + } > + > + if (!have_icc_profile) { > + int bits_remaining = 7 - (gb->index - 1) % 8; Try to avoid accessing GetBitContext members directly; here: Use get_bits_count(). > + if (bits_remaining && jxl_bits(bits_remaining)) > + return -1; > + } > + > + if (gb->index > gb->size_in_bits) get_bits_left(gb) < 0 > + return -1; > + > + return 0; > +} For the record: I'm not really ok with duplicating this code in lavf and lavc. > diff --git a/libavformat/jpegxl_probe.h b/libavformat/jpegxl_probe.h > new file mode 100644 > index 0000000000..2960e81e11 > --- /dev/null > +++ b/libavformat/jpegxl_probe.h > @@ -0,0 +1,32 @@ > +/* > + * Jpeg XL header verification > + * Copyright (c) 2022 Leo Izen <leo.i...@gmail.com> > + * > + * This file is part of FFmpeg. > + * > + * FFmpeg is free software; you can redistribute it and/or > + * modify it under the terms of the GNU Lesser General Public > + * License as published by the Free Software Foundation; either > + * version 2.1 of the License, or (at your option) any later version. > + * > + * FFmpeg is distributed in the hope that it will be useful, > + * but WITHOUT ANY WARRANTY; without even the implied warranty of > + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU > + * Lesser General Public License for more details. > + * > + * You should have received a copy of the GNU Lesser General Public > + * License along with FFmpeg; if not, write to the Free Software > + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 > USA > + */ > + > +#ifndef AVFORMAT_JPEGXL_PROBE_H > +#define AVFORMAT_JPEGXL_PROBE_H > + > +#include <stdint.h> > + > +#define FF_JPEGXL_CODESTREAM_SIGNATURE_LE 0x0aff > +#define FF_JPEGXL_CONTAINER_SIGNATURE_LE 0x204c584a0c000000 > + > +int ff_jpegxl_verify_codestream_header(const uint8_t *buf, int buflen); > + > +#endif /* AVFORMAT_JPEGXL_PROBE_H */ > diff --git a/libavformat/mov.c b/libavformat/mov.c > index 6c847de164..c4b8873b0a 100644 > --- a/libavformat/mov.c > +++ b/libavformat/mov.c > @@ -7697,6 +7697,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 { _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-requ...@ffmpeg.org with subject "unsubscribe".