Commit 20c7c43b for libheif

commit 20c7c43b2ef67ad7ffb18a8b4af92340e85df5e7
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Tue May 19 13:14:16 2026 +0200

    add auto-correct option to fix known input image errors (#1770)

diff --git a/examples/heif_dec.cc b/examples/heif_dec.cc
index 84fe1e78..5f51e657 100644
--- a/examples/heif_dec.cc
+++ b/examples/heif_dec.cc
@@ -137,6 +137,7 @@ static void show_help(const char* argv0)
                "      --transparency-composition-mode MODE  Controls how transparent images are rendered when the output format\n"
                "                                            support transparency. MODE must be one of: white, black, checkerboard.\n"
                "      --disable-limits           disable all security limits (do not use in production environment)\n"
+               "      --auto-correct             work around known broken-input quirks (e.g. Sony HIF full-range flag mismatch)\n"
                "      --codec-threads #          number of threads to use in the codec plugin (0 = default)\n"
             << "      --tile-threads #           max number of tiles to decode in parallel (default = " << default_tile_threads << ")\n"
             << "      --extract-mime-item TYPE   extract the MIME item with the given content type into a file (mime-item.data)\n";
@@ -169,6 +170,7 @@ int option_list_decoders = 0;
 int option_png_compression_level = -1; // use zlib default
 int option_output_tiles = 0;
 int option_disable_limits = 0;
+int option_auto_correct = 0;
 int option_sequence = 0;
 int option_ignore_editlist = 0;
 int option_num_codec_threads = 0;
@@ -205,6 +207,7 @@ static option long_options[] = {
     {(char* const) "transparency-composition-mode", required_argument, 0,  OPTION_TRANSPARENCY_COMPOSITION_MODE},
     {(char* const) "version",          no_argument,       0,                        'v'},
     {(char* const) "disable-limits", no_argument, &option_disable_limits, 1},
+    {(char* const) "auto-correct",   no_argument, &option_auto_correct,   1},
     {(char* const) "ignore-editlist", no_argument, &option_ignore_editlist, 1},
     {(char* const) "codec-threads", required_argument, 0,                     OPTION_CODEC_THREADS},
     {(char* const) "tile-threads",  required_argument, 0,                     OPTION_TILE_THREADS},
@@ -920,6 +923,7 @@ int main(int argc, char** argv)
     decode_options->strict_decoding = strict_decoding;
     decode_options->decoder_id = decoder_id;
     decode_options->num_codec_threads = option_num_codec_threads;
+    decode_options->autocorrect_broken_input = (option_auto_correct != 0);

     heif_track* track = heif_context_get_track(ctx, 0);

@@ -1044,6 +1048,7 @@ int main(int argc, char** argv)
     decode_options->strict_decoding = strict_decoding;
     decode_options->decoder_id = decoder_id;
     decode_options->num_codec_threads = option_num_codec_threads;
+    decode_options->autocorrect_broken_input = (option_auto_correct != 0);

     if (!option_quiet) {
       decode_options->start_progress = start_progress;
diff --git a/libheif/api/libheif/heif_decoding.cc b/libheif/api/libheif/heif_decoding.cc
index 575790b9..ce0702b1 100644
--- a/libheif/api/libheif/heif_decoding.cc
+++ b/libheif/api/libheif/heif_decoding.cc
@@ -52,7 +52,7 @@ int heif_have_decoder_for_format(heif_compression_format format)

 static void fill_default_decoding_options(heif_decoding_options& options)
 {
-  options.version = 8;
+  options.version = 9;

   options.ignore_transformations = false;

@@ -94,6 +94,10 @@ static void fill_default_decoding_options(heif_decoding_options& options)
   options.output_image_nclx_profile = nullptr;
   options.num_codec_threads = 0;
   options.num_library_threads = 0;
+
+  // version 9
+
+  options.autocorrect_broken_input = false;
 }


@@ -118,6 +122,9 @@ void heif_decoding_options_copy(heif_decoding_options* dst,
   int min_version = std::min(dst->version, src->version);

   switch (min_version) {
+    case 9:
+      dst->autocorrect_broken_input = src->autocorrect_broken_input;
+      [[fallthrough]];
     case 8:
       dst->num_library_threads = src->num_library_threads;
       dst->num_codec_threads = src->num_codec_threads;
diff --git a/libheif/api/libheif/heif_decoding.h b/libheif/api/libheif/heif_decoding.h
index 3f1e8902..43059fdf 100644
--- a/libheif/api/libheif/heif_decoding.h
+++ b/libheif/api/libheif/heif_decoding.h
@@ -118,6 +118,13 @@ typedef struct heif_decoding_options

   int num_library_threads; // 0 = let libheif decide (TODO, currently ignored)
   int num_codec_threads; // 0 = use decoder default
+
+  // version 9 options
+
+  // If enabled, libheif will attempt to work around known broken-input quirks
+  // (e.g. Sony HIF files where the NCLX `colr` box disagrees with the HEVC VUI
+  // on the YCbCr range flag). Default: false (strict spec-conformant behavior).
+  uint8_t autocorrect_broken_input;
 } heif_decoding_options;


diff --git a/libheif/image-items/image_item.cc b/libheif/image-items/image_item.cc
index 688fb09b..e9cdb585 100644
--- a/libheif/image-items/image_item.cc
+++ b/libheif/image-items/image_item.cc
@@ -1061,30 +1061,38 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
   // If there is an NCLX profile in the HEIF/AVIF metadata, use this for the color conversion.
   // Otherwise, use the profile that is stored in the image stream itself and then set the
   // (non-NCLX) profile later.
-  auto nclx = get_color_profile_nclx();
-  if (!nclx.is_undefined()) {
+  const auto heif_nclx = get_color_profile_nclx();
+  if (heif_nclx.is_defined()) {
+
+    // Since we have a HEIF colr box, we overwrite the bitstream's CICP parameter
+    // with that parameter from the colr box.
+    nclx_profile consolidated_nclx = heif_nclx;
+
     // 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 and ISO/IEC 23000-22 (MIAF)
     // 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, see issue #1770) and is
-    // worth surfacing as a warning.
-    auto bitstream_nclx = img->get_color_profile_nclx();
-    if (!bitstream_nclx.is_undefined()) {
+    // indication of a muxer bug.
+    const auto bitstream_nclx = img->get_color_profile_nclx();
+    if (bitstream_nclx.is_defined()) {
+
+      // Check whether there is a CICP mismatch between the HEIF colr box and the compressed bitstream
+      // If yes, output a warning.
+
       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) {
+
+      if (cicp_mismatch(bitstream_nclx.m_colour_primaries,        heif_nclx.m_colour_primaries)        ||
+          cicp_mismatch(bitstream_nclx.m_transfer_characteristics, heif_nclx.m_transfer_characteristics) ||
+          cicp_mismatch(bitstream_nclx.m_matrix_coefficients,     heif_nclx.m_matrix_coefficients)     ||
+          bitstream_nclx.m_full_range_flag != heif_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")
+            << heif_nclx.m_colour_primaries << "/"
+            << heif_nclx.m_transfer_characteristics << "/"
+            << heif_nclx.m_matrix_coefficients << "/"
+            << (heif_nclx.m_full_range_flag ? "full" : "limited")
             << ") disagrees with bitstream signalling ("
             << bitstream_nclx.m_colour_primaries << "/"
             << bitstream_nclx.m_transfer_characteristics << "/"
@@ -1095,8 +1103,29 @@ Result<std::shared_ptr<HeifPixelImage>> ImageItem::decode_image(const heif_decod
                               heif_suberror_NCLX_colr_VUI_mismatch,
                               msg.str()});
       }
+
+      // Fix full-range flag in images that are probably broken.
+
+      if (options.version >= 9 && options.autocorrect_broken_input) {
+
+        // Some Sony cameras mis-tag full_range_flag=0 in colr while the bitstream VUI is correct (full_range_flag=1), see issue #1770.
+        // Rationale for the fix: if the bitstream explicitly says full_range=1, it probably does with for a reason.
+        // Thus, we keep the full-range flag.
+
+        if (bitstream_nclx.get_full_range_flag() == true &&
+            heif_nclx.get_full_range_flag() == false) {
+          add_decoding_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);
+        }
+      }
     }
-    img->set_color_profile_nclx(nclx);
+
+    img->set_color_profile_nclx(consolidated_nclx);
   }

   auto icc = get_color_profile_icc();
diff --git a/libheif/nclx.h b/libheif/nclx.h
index 715fa5f8..1192cd79 100644
--- a/libheif/nclx.h
+++ b/libheif/nclx.h
@@ -155,6 +155,8 @@ struct nclx_profile

   bool is_undefined() const;

+  bool is_defined() const { return !is_undefined(); }
+
   void replace_undefined_values_with_sRGB_defaults();

   bool equal_except_transfer_curve(const nclx_profile& b) const;