Commit 8797a50e for libheif

commit 8797a50e85ec072685054a349bd6a0bced040f27
Author: Dirk Lemstra <dirk@lemstra.org>
Date:   Sun Oct 11 21:47:42 2026 +0200

    Honor NCLX range autocorrection when decoding sequences

diff --git a/libheif/sequences/track_visual.cc b/libheif/sequences/track_visual.cc
index e97838fa..6c0b10be 100644
--- a/libheif/sequences/track_visual.cc
+++ b/libheif/sequences/track_visual.cc
@@ -368,10 +368,28 @@ Result<std::shared_ptr<HeifPixelImage> > Track_Visual::decode_next_image_sample(
   const SampleTiming& outputTiming = m_presentation_timeline[m_next_sample_to_be_output % m_presentation_timeline.size()];
   const auto* sampleToChunk = m_stsc->get_chunk(outputTiming.chunkIdx + 1);
   auto sampleDescription = m_stsd->get_sample_entry(sampleToChunk->sample_description_index - 1);
+  const auto bitstream_nclx = image->get_color_profile_nclx();

   // Sample-entry colour information takes precedence over bitstream metadata.
   for (const auto& colr : sampleDescription->get_child_boxes<Box_colr>()) {
-    image->set_color_profile(colr->get_color_profile());
+    auto profile = colr->get_color_profile();
+    if (auto nclx = std::dynamic_pointer_cast<const color_profile_nclx>(profile)) {
+      auto consolidated_nclx = nclx->get_nclx_color_profile();
+      if (options.version >= 9 && options.autocorrect_broken_input &&
+          bitstream_nclx.is_defined() && bitstream_nclx.get_full_range_flag() &&
+          !consolidated_nclx.get_full_range_flag()) {
+        image->add_warning({
+          heif_error_Invalid_input,
+          heif_suberror_NCLX_colr_VUI_mismatch,
+          "Autocorrecting full-range flag to ON (colr=limited, bitstream=full)"
+        });
+        consolidated_nclx.set_full_range_flag(true);
+      }
+      image->set_color_profile_nclx(consolidated_nclx);
+    }
+    else {
+      image->set_color_profile(profile);
+    }
   }
   if (auto clli = sampleDescription->get_child_box<Box_clli>()) {
     image->set_clli(clli->clli);
diff --git a/tests/image_description_metadata.cc b/tests/image_description_metadata.cc
index 7b32e0ed..27f6f54c 100644
--- a/tests/image_description_metadata.cc
+++ b/tests/image_description_metadata.cc
@@ -316,8 +316,15 @@ static std::vector<uint8_t> encode_metadata_sequence()
     }
   }
   heif_image_set_duration(image, 1);
+  heif_color_profile_nclx* nclx = heif_nclx_color_profile_alloc();
+  REQUIRE(nclx != nullptr);
+  nclx->full_range_flag = 1;
+  REQUIRE(heif_image_set_nclx_color_profile(image, nclx).code == heif_error_Ok);
+  heif_sequence_encoding_options* encoding_options = heif_sequence_encoding_options_alloc();
+  REQUIRE(encoding_options != nullptr);
+  encoding_options->output_nclx_profile = nclx;
   for (int frame = 0; frame < 3; ++frame) {
-    REQUIRE(heif_track_encode_sequence_image(track, image, encoder, nullptr).code == heif_error_Ok);
+    REQUIRE(heif_track_encode_sequence_image(track, image, encoder, encoding_options).code == heif_error_Ok);
   }
   REQUIRE(heif_track_encode_end_of_sequence(track, encoder).code == heif_error_Ok);
   std::vector<uint8_t> file;
@@ -325,6 +332,8 @@ static std::vector<uint8_t> encode_metadata_sequence()
   writer.writer_api_version = 1;
   writer.write = sequence_metadata_writer;
   REQUIRE(heif_context_write(context, &writer, &file).code == heif_error_Ok);
+  heif_sequence_encoding_options_release(encoding_options);
+  heif_nclx_color_profile_free(nclx);
   heif_image_release(image);
   heif_track_release(track);
   heif_encoder_release(encoder);
@@ -398,7 +407,9 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
   }
   const bool with_metadata = GENERATE(false, true);
   const bool convert_to_rgb = GENERATE(false, true);
+  const bool autocorrect = GENERATE(false, true);
   INFO("sample-entry metadata: " << with_metadata << ", RGB conversion: " << convert_to_rgb);
+  INFO("autocorrect: " << autocorrect);
   auto file = encode_metadata_sequence();
   const std::vector<uint8_t> icc = {1, 2, 3, 4, 5};
   if (with_metadata) {
@@ -411,6 +422,7 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
   heif_decoding_options* options = heif_decoding_options_alloc();
   options->ignore_sequence_editlist = 1;
   options->output_image_nclx_profile_passthrough = 1;
+  options->autocorrect_broken_input = autocorrect;
   for (int frame = 0; frame < 3; ++frame) {
     INFO("frame: " << frame);
     heif_image* image = nullptr;
@@ -419,12 +431,17 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
                                          convert_to_rgb ? heif_chroma_444 : heif_chroma_undefined,
                                          options).code == heif_error_Ok);
     REQUIRE(image != nullptr);
+    if (with_metadata && autocorrect) {
+      heif_error warning{};
+      REQUIRE(heif_image_get_decoding_warnings(image, 0, &warning, 1) == 1);
+      CHECK(warning.subcode == heif_suberror_NCLX_colr_VUI_mismatch);
+    }
     if (with_metadata && convert_to_rgb) {
       size_t stride = 0;
       const uint8_t* pixels = heif_image_get_plane_readonly2(image, heif_channel_R, &stride);
       REQUIRE(pixels != nullptr);
-      CHECK(pixels[0] >= 55);
-      CHECK(pixels[0] <= 57);
+      CHECK(pixels[0] >= (autocorrect ? 63 : 55));
+      CHECK(pixels[0] <= (autocorrect ? 65 : 57));
     }
     if (with_metadata && !convert_to_rgb) {
       heif_color_profile_nclx* nclx = nullptr;
@@ -432,7 +449,7 @@ TEST_CASE("sequence sample-entry colour metadata is applied to every decoded fra
       CHECK(nclx->color_primaries == 9);
       CHECK(nclx->transfer_characteristics == 16);
       CHECK(nclx->matrix_coefficients == 9);
-      CHECK(nclx->full_range_flag == 0);
+      CHECK(nclx->full_range_flag == (autocorrect ? 1 : 0));
       heif_nclx_color_profile_free(nclx);
     }
     CHECK(heif_image_get_raw_color_profile_size(image) == (with_metadata ? icc.size() : 0));