Commit 51b61160 for libheif

commit 51b6116054616e80c2f2cc8dd5ac1f402296d4a4
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Fri May 15 23:22:27 2026 +0200

    show warning when nclx and bitstream VUI disagree (#1770)

diff --git a/libheif/api/libheif/heif_error.h b/libheif/api/libheif/heif_error.h
index 29dc48a4..29a09f10 100644
--- a/libheif/api/libheif/heif_error.h
+++ b/libheif/api/libheif/heif_error.h
@@ -191,6 +191,10 @@ typedef enum heif_suberror_code

   heif_suberror_No_moov_box = 151,

+  // The colr (NCLX) box and the codec bitstream VUI/color signalling disagree.
+  // Per ISO/IEC 14496-12 the colr box takes precedence, but the conflict is reported as a warning.
+  heif_suberror_NCLX_colr_VUI_mismatch = 152,
+
   // --- Memory_allocation_error ---

   // A security limit preventing unreasonable memory allocations was exceeded by the input file.
diff --git a/libheif/error.cc b/libheif/error.cc
index 32da1e5e..63228d14 100644
--- a/libheif/error.cc
+++ b/libheif/error.cc
@@ -229,6 +229,8 @@ const char* Error::get_error_string(heif_suberror_code err)
       return "Invalid data in generic compression inflation";
     case heif_suberror_No_moov_box:
       return "No 'moov' box";
+    case heif_suberror_NCLX_colr_VUI_mismatch:
+      return "colr box and bitstream colour signalling disagree";
     case heif_suberror_No_icbr_box:
       return "No 'icbr' box";
     case heif_suberror_Invalid_mini_box:
diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 2a8b0346..9314cb40 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -40,6 +40,7 @@
 #include <limits>
 #include <cassert>
 #include <cstring>
+#include <sstream>
 //#include <ranges>

 #if WITH_UNCOMPRESSED_CODEC
@@ -1039,6 +1040,38 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
   // (non-NCLX) profile later.
   auto nclx = get_color_profile_nclx();
   if (!nclx.is_undefined()) {
+    // If the decoder plugin populated an NCLX profile from the bitstream's
+    // color signalling (e.g. HEVC SPS VUI, AV1 sequence header), compare it
+    // against the colr box. Per ISO/IEC 14496-12 §12.1.5.1 the colr box
+    // overrides the bitstream, but a mismatch is a strong indication of a
+    // muxer bug (e.g. some Sony cameras mis-tag full_range_flag in colr while
+    // the bitstream VUI is correct) and is worth surfacing as a warning.
+    auto bitstream_nclx = img->get_color_profile_nclx();
+    if (!bitstream_nclx.is_undefined()) {
+      auto cicp_mismatch = [](uint16_t bs, uint16_t cr) {
+        return bs != 2 /*unspecified*/ && cr != 2 && bs != cr;
+      };
+      if (cicp_mismatch(bitstream_nclx.m_colour_primaries,        nclx.m_colour_primaries)        ||
+          cicp_mismatch(bitstream_nclx.m_transfer_characteristics, nclx.m_transfer_characteristics) ||
+          cicp_mismatch(bitstream_nclx.m_matrix_coefficients,     nclx.m_matrix_coefficients)     ||
+          bitstream_nclx.m_full_range_flag != nclx.m_full_range_flag) {
+        std::stringstream msg;
+        msg << "colr box NCLX ("
+            << nclx.m_colour_primaries << "/"
+            << nclx.m_transfer_characteristics << "/"
+            << nclx.m_matrix_coefficients << "/"
+            << (nclx.m_full_range_flag ? "full" : "limited")
+            << ") disagrees with bitstream signalling ("
+            << bitstream_nclx.m_colour_primaries << "/"
+            << bitstream_nclx.m_transfer_characteristics << "/"
+            << bitstream_nclx.m_matrix_coefficients << "/"
+            << (bitstream_nclx.m_full_range_flag ? "full" : "limited")
+            << "); colr takes precedence per ISO/IEC 14496-12";
+        add_decoding_warning({heif_error_Invalid_input,
+                              heif_suberror_NCLX_colr_VUI_mismatch,
+                              msg.str()});
+      }
+    }
     img->set_color_profile_nclx(nclx);
   }

diff --git a/libheif/image-items/image_item.h b/libheif/image-items/image_item.h
index 0a390f06..cc7ddefe 100644
--- a/libheif/image-items/image_item.h
+++ b/libheif/image-items/image_item.h
@@ -417,7 +417,7 @@ public:
   const std::vector<heif_item_id>& get_region_item_ids() const { return m_region_item_ids; }


-  void add_decoding_warning(Error err) { m_decoding_warnings.emplace_back(std::move(err)); }
+  void add_decoding_warning(Error err) const { m_decoding_warnings.emplace_back(std::move(err)); }

   const std::vector<Error>& get_decoding_warnings() const { return m_decoding_warnings; }

@@ -482,7 +482,7 @@ private:
   bool m_has_extrinsic_matrix = false;
   Box_cmex::ExtrinsicMatrix m_extrinsic_matrix{};

-  std::vector<Error> m_decoding_warnings;
+  mutable std::vector<Error> m_decoding_warnings;

   mutable std::mutex m_decode_mutex;