Commit a6bc4a42 for libheif
commit a6bc4a42dd6640b31d8395a19d4925c5abd847a1
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Thu Jun 25 14:38:55 2026 +0200
FFmpeg decoder: gate HTJ2K decoding to FFmpeg 7.0+ (#1846)
FFmpeg gained High-Throughput JPEG 2000 (HT block coder) decoding only in
7.0 (libavcodec 61); earlier versions silently misdecode HT codestreams.
Gate HTJ2K in does_support_format() so an older FFmpeg is not advertised
for it. HEIF reports both plain and HT JPEG 2000 as heif_compression_JPEG2000
on read, so an HT codestream can still reach the plugin via the JPEG2000
path; detect the HT CAP marker (0xFF50) in the codestream and refuse with a
clear error instead of producing garbage.
diff --git a/libheif/plugins/decoder_ffmpeg.cc b/libheif/plugins/decoder_ffmpeg.cc
index a5404c75..0f7f05cb 100644
--- a/libheif/plugins/decoder_ffmpeg.cc
+++ b/libheif/plugins/decoder_ffmpeg.cc
@@ -116,8 +116,15 @@ static int ffmpeg_does_support_format(heif_compression_format format)
case heif_compression_JPEG:
return avcodec_find_decoder(AV_CODEC_ID_MJPEG) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0;
case heif_compression_JPEG2000:
+ return avcodec_find_decoder(AV_CODEC_ID_JPEG2000) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0;
case heif_compression_HTJ2K:
+ // FFmpeg gained High-Throughput JPEG 2000 (HT block coder) decoding in 7.0
+ // (libavcodec 61). Earlier versions silently misdecode the HT codestream.
+#if LIBAVCODEC_VERSION_MAJOR >= 61
return avcodec_find_decoder(AV_CODEC_ID_JPEG2000) ? FFMPEG_DECODER_PLUGIN_PRIORITY : 0;
+#else
+ return 0;
+#endif
default:
return 0;
}
@@ -212,12 +219,70 @@ void ffmpeg_set_strict_decoding(void* decoder_raw, int flag)
decoder->strict_decoding = flag;
}
+#if LIBAVCODEC_VERSION_MAJOR < 61
+// Scan a JPEG 2000 codestream main header for the CAP marker (0xFF50), which
+// signals the High-Throughput (HT) block coder (JPEG 2000 Part 15). FFmpeg
+// gained HT decoding only in 7.0 (libavcodec 61); older versions silently
+// misdecode the stream, so we use this to refuse HT input on those.
+static bool jpeg2000_codestream_uses_HT(const uint8_t* data, size_t size)
+{
+ // main header must start with the SOC marker (FF4F)
+ if (size < 2 || data[0] != 0xFF || data[1] != 0x4F) {
+ return false;
+ }
+
+ size_t pos = 2;
+ while (pos + 2 <= size) {
+ if (data[pos] != 0xFF) {
+ break; // not positioned on a marker -> stop scanning
+ }
+
+ uint16_t marker = (uint16_t(data[pos]) << 8) | data[pos + 1];
+
+ if (marker == 0xFF50) { // CAP
+ return true;
+ }
+ if (marker == 0xFF90 || // SOT -> main header finished
+ marker == 0xFFD9) { // EOC
+ break;
+ }
+
+ // All remaining main-header markers carry a 2-byte segment length.
+ if (pos + 4 > size) {
+ break;
+ }
+ uint16_t seg_len = (uint16_t(data[pos + 2]) << 8) | data[pos + 3];
+ if (seg_len < 2) {
+ break; // malformed
+ }
+ pos += 2 + seg_len;
+ }
+
+ return false;
+}
+#endif
+
+
static heif_error ffmpeg_push_data2(void *decoder_raw, const void *data, size_t size, uintptr_t user_data)
{
ffmpeg_decoder* decoder = (struct ffmpeg_decoder*) decoder_raw;
const uint8_t* cdata = (const uint8_t*) data;
+#if LIBAVCODEC_VERSION_MAJOR < 61
+ // HEIF reports both plain and HT JPEG 2000 as heif_compression_JPEG2000, so an
+ // HT codestream can reach this plugin even though does_support_format() gates
+ // HTJ2K off. Detect it from the codestream and refuse, rather than emit garbage.
+ if (decoder->av_codec->id == AV_CODEC_ID_JPEG2000 &&
+ jpeg2000_codestream_uses_HT(cdata, size)) {
+ return {
+ heif_error_Unsupported_feature,
+ heif_suberror_Unsupported_data_version,
+ "High-Throughput JPEG 2000 decoding requires FFmpeg 7.0 or later"
+ };
+ }
+#endif
+
ffmpeg_decoder::Packet pkt;
size_t ptr = 0;