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();
}