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;