Commit 6929ee1c for libheif

commit 6929ee1c6431ad909a95625e202f5505edef3477
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu Jun 25 14:24:16 2026 +0200

    FFmpeg decoder: interpret JPEG2000 output as YCbCr (#1846)

    FFmpeg's JPEG2000 decoder returns the codestream's three components as
    packed rgb24. libheif stores J2K as sYCC, so these components are really
    Y/Cb/Cr. De-interleave them into a YCbCr-444 image so the container's
    nclx color conversion is applied, instead of treating them as RGB.

diff --git a/libheif/plugins/decoder_ffmpeg.cc b/libheif/plugins/decoder_ffmpeg.cc
index 53a64e33..a5404c75 100644
--- a/libheif/plugins/decoder_ffmpeg.cc
+++ b/libheif/plugins/decoder_ffmpeg.cc
@@ -390,6 +390,42 @@ static heif_error ffmpeg_av_decode(ffmpeg_decoder* decoder, AVCodecContext* av_d
     *out_user_data = av_frame->pts;
   }

+  // FFmpeg's JPEG2000 decoder hands back the codestream's 3 components as packed
+  // "rgb24", but libheif stores them as sYCC (the codestream is tagged sYCC).
+  // De-interleave into YCbCr-444 planes so the container's nclx color conversion
+  // is applied correctly.
+  if (av_dec_ctx->pix_fmt == AV_PIX_FMT_RGB24 &&
+      decoder->av_codec->id == AV_CODEC_ID_JPEG2000) {
+    heif_error err = heif_image_create(av_frame->width, av_frame->height,
+                                       heif_colorspace_YCbCr, heif_chroma_444, image);
+    if (err.code) {
+      return err;
+    }
+    heif_channel planes[3] = {heif_channel_Y, heif_channel_Cb, heif_channel_Cr};
+    uint8_t* dst[3]; size_t dst_stride[3];
+    for (int c = 0; c < 3; c++) {
+      err = heif_image_add_plane_safe(*image, planes[c], av_frame->width, av_frame->height, 8, limits);
+      if (err.code) {
+        decoder->error_message = err.message;
+        err.message = decoder->error_message.c_str();
+        heif_image_release(*image);
+        return err;
+      }
+      dst[c] = heif_image_get_plane2(*image, planes[c], &dst_stride[c]);
+    }
+    const uint8_t* src = av_frame->data[0];
+    int src_stride = av_frame->linesize[0];
+    for (int y = 0; y < av_frame->height; y++) {
+      const uint8_t* row = src + static_cast<size_t>(y) * src_stride;
+      for (int x = 0; x < av_frame->width; x++) {
+        dst[0][y * dst_stride[0] + x] = row[x * 3 + 0];  // Y  <- "R"
+        dst[1][y * dst_stride[1] + x] = row[x * 3 + 1];  // Cb <- "G"
+        dst[2][y * dst_stride[2] + x] = row[x * 3 + 2];  // Cr <- "B"
+      }
+    }
+    return heif_error_success;
+  }
+
   heif_chroma chroma = ffmpeg_get_chroma_format(av_dec_ctx->pix_fmt);
   if (chroma != heif_chroma_undefined) {
     bool is_mono = (chroma == heif_chroma_monochrome);