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;