Commit 9502996a for libheif

commit 9502996a40f076391cd637867cc25a26cbf23891
Author: Dirk Farin <dirk.farin@gmail.com>
Date:   Thu May 28 21:24:15 2026 +0200

    add heif_decoding_options flag for nclx passthrough (#1828)

diff --git a/heifio/decoder_heif.cc b/heifio/decoder_heif.cc
index 3258c49d..4bea2164 100644
--- a/heifio/decoder_heif.cc
+++ b/heifio/decoder_heif.cc
@@ -82,6 +82,7 @@ heif_error loadHEIF(const char* filename, InputImage* input_image)
   // already in display orientation and input_image->orientation stays 'normal'.

   heif_decoding_options* opts = heif_decoding_options_alloc();
+  opts->output_image_nclx_profile_passthrough = true;

   heif_image* image = nullptr;
   err = heif_decode_image(handle, &image,
diff --git a/libheif/api/libheif/heif_decoding.cc b/libheif/api/libheif/heif_decoding.cc
index ce0702b1..2b845816 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 = 9;
+  options.version = 10;

   options.ignore_transformations = false;

@@ -98,6 +98,10 @@ static void fill_default_decoding_options(heif_decoding_options& options)
   // version 9

   options.autocorrect_broken_input = false;
+
+  // version 10
+
+  options.output_image_nclx_profile_passthrough = false;
 }


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

   switch (min_version) {
+    case 10:
+      dst->output_image_nclx_profile_passthrough = src->output_image_nclx_profile_passthrough;
+      [[fallthrough]];
     case 9:
       dst->autocorrect_broken_input = src->autocorrect_broken_input;
       [[fallthrough]];
diff --git a/libheif/api/libheif/heif_decoding.h b/libheif/api/libheif/heif_decoding.h
index 43059fdf..34c9e4fc 100644
--- a/libheif/api/libheif/heif_decoding.h
+++ b/libheif/api/libheif/heif_decoding.h
@@ -114,6 +114,15 @@ typedef struct heif_decoding_options
   // If enabled, it will decode the media timeline, ignoring the sequence tracks edit-list.
   int ignore_sequence_editlist; // bool

+  // Requested NCLX color profile of the decoded output image. If the input
+  // image's NCLX differs, libheif will color-convert the pixels accordingly
+  // (e.g. YCbCr matrix, primaries, range) so the result matches what is
+  // requested here.
+  //
+  // When set to NULL, the behavior depends on the flag
+  // output_image_nclx_profile_passthrough below: by default NULL means
+  // "convert to sRGB"; with the passthrough flag enabled, NULL means
+  // "keep the input image's NCLX".
   heif_color_profile_nclx* output_image_nclx_profile;

   int num_library_threads; // 0 = let libheif decide (TODO, currently ignored)
@@ -125,6 +134,27 @@ typedef struct heif_decoding_options
   // (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;
+
+  // version 10 options
+
+  // Controls the meaning of output_image_nclx_profile == NULL.
+  //
+  // When false (default), a NULL output_image_nclx_profile means "convert the
+  // decoded image to sRGB" (BT.709 primaries, sRGB transfer, BT.601 matrix,
+  // full-range). For HDR inputs (e.g. BT.2100 PQ) this silently discards the
+  // original color volume.
+  //
+  // When true, a NULL output_image_nclx_profile means "keep the input image's
+  // NCLX". The decoded image carries the input file's primaries / transfer /
+  // matrix / range, and no extra color-space conversion is performed solely
+  // because the output NCLX was unspecified. If a YCbCr<->RGB colorspace
+  // conversion fires for another reason, the input NCLX is used to drive that
+  // conversion (so the result is tagged consistently with the source).
+  //
+  // Although this flag is off by default to preserve historical behavior, new
+  // code that wants to preserve HDR through decode should generally enable it.
+  // Setting output_image_nclx_profile to a non-NULL value overrides this flag.
+  uint8_t output_image_nclx_profile_passthrough;
 } heif_decoding_options;


diff --git a/libheif/context.cc b/libheif/context.cc
index 8cd066cb..eb22f642 100644
--- a/libheif/context.cc
+++ b/libheif/context.cc
@@ -1523,7 +1523,10 @@ Result<std::shared_ptr<HeifPixelImage>> HeifContext::convert_to_output_colorspac
   uint8_t converted_output_bpp = (options.convert_hdr_to_8bit && img_bpp > 8) ? 8 : 0 /* keep input depth */;

   nclx_profile img_nclx = img->get_color_profile_nclx_with_fallback();
-  bool different_nclx = !nclx_color_profile_equal(img_nclx, options.output_image_nclx_profile);
+  const bool nclx_passthrough = (options.output_image_nclx_profile == nullptr &&
+                                 options.output_image_nclx_profile_passthrough);
+  const bool different_nclx = !nclx_passthrough &&
+                              !nclx_color_profile_equal(img_nclx, options.output_image_nclx_profile);

   if (different_chroma ||
       different_colorspace ||
@@ -1537,6 +1540,11 @@ Result<std::shared_ptr<HeifPixelImage>> HeifContext::convert_to_output_colorspac
       output_profile.set_colour_primaries(options.output_image_nclx_profile->color_primaries);
       output_profile.set_full_range_flag(options.output_image_nclx_profile->full_range_flag);
     }
+    else if (nclx_passthrough) {
+      // Keep input image's NCLX as the conversion target so a chroma/colorspace
+      // change does not silently re-tag the pixels with sRGB.
+      output_profile = img_nclx;
+    }
     else {
       output_profile.set_sRGB_defaults();
     }