Commit ef257891f for imagemagick.org
commit ef257891f813a70d45b6abc7172c059d024a9c17
Author: Greg B <64932474+gregbenz@users.noreply.github.com>
Date: Sat Jul 4 02:38:27 2026 -0500
Fix HEIC identity CICP with chroma subsampling (#8828)
Co-authored-by: Greg Benz <git@gregbenzphotography.com>
diff --git a/coders/heic.c b/coders/heic.c
index 91804545c..b3bac327b 100644
--- a/coders/heic.c
+++ b/coders/heic.c
@@ -1407,8 +1407,76 @@ static void WriteProfile(struct heif_context *context,Image *image,
}
#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
-static MagickBooleanType WriteHEICCICPProfile(Image *image,
- struct heif_image *heif_image,const char *option,ExceptionInfo *exception)
+static const char *GetHEICCICPOption(const ImageInfo *image_info,Image *image,
+ ExceptionInfo *exception)
+{
+ const char
+ *option;
+
+ option=GetImageOption(image_info,"heic:cicp");
+ if ((option == (const char *) NULL) &&
+ (GetImageProfile(image,"icc") == (const StringInfo *) NULL) &&
+ (IsStringFalse(GetImageOption(image_info,"heic:preserve-cicp")) ==
+ MagickFalse))
+ option=GetImageProperty(image,"heic:cicp",exception);
+ return(option);
+}
+
+static MagickBooleanType IsHEICCICPIdentityMatrix(const char *option)
+{
+ GeometryInfo
+ cicp;
+
+ MagickStatusType
+ flags;
+
+ if (option == (const char *) NULL)
+ return(MagickFalse);
+ SetGeometryInfo(&cicp);
+ flags=ParseGeometry(option,&cicp);
+ if (((flags & XiValue) != 0) && (cicp.xi == 0.0))
+ return(MagickTrue);
+ return(MagickFalse);
+}
+
+static MagickBooleanType IsHEICExplicitCICP(const ImageInfo *image_info)
+{
+ if (GetImageOption(image_info,"heic:cicp") != (const char *) NULL)
+ return(MagickTrue);
+ return(MagickFalse);
+}
+
+static MagickBooleanType IsHEICSubsampledChroma(const char *option)
+{
+ if (option == (const char *) NULL)
+ return(MagickFalse);
+ if ((LocaleCompare(option,"420") == 0) ||
+ (LocaleCompare(option,"422") == 0))
+ return(MagickTrue);
+ return(MagickFalse);
+}
+
+static MagickBooleanType HEICImageHasIdentityCICP(
+ const ImageInfo *image_info,Image *image,MagickBooleanType image_list,
+ ExceptionInfo *exception)
+{
+ Image
+ *p;
+
+ for (p=image; p != (Image *) NULL; p=GetNextImageInList(p))
+ {
+ if (IsHEICCICPIdentityMatrix(GetHEICCICPOption(image_info,p,
+ exception)) != MagickFalse)
+ return(MagickTrue);
+ if (image_list == MagickFalse)
+ break;
+ }
+ return(MagickFalse);
+}
+
+static struct heif_color_profile_nclx *CreateHEICCICPProfile(Image *image,
+ const char *option,MagickBooleanType use_ycbcr_matrix,
+ ExceptionInfo *exception)
{
GeometryInfo
cicp;
@@ -1424,7 +1492,7 @@ static MagickBooleanType WriteHEICCICPProfile(Image *image,
{
(void) ThrowMagickException(exception,GetMagickModule(),
ResourceLimitError,"MemoryAllocationFailed","`%s'",image->filename);
- return(MagickFalse);
+ return((struct heif_color_profile_nclx *) NULL);
}
SetGeometryInfo(&cicp);
cicp.rho=(double) nclx_profile->color_primaries;
@@ -1432,28 +1500,76 @@ static MagickBooleanType WriteHEICCICPProfile(Image *image,
cicp.xi=(double) nclx_profile->matrix_coefficients;
cicp.psi=(double) nclx_profile->full_range_flag;
(void) ParseGeometry(option,&cicp);
+ if (use_ycbcr_matrix != MagickFalse)
+ cicp.xi=(double) heif_matrix_coefficients_ITU_R_BT_601_6;
error=heif_nclx_color_profile_set_color_primaries(nclx_profile,
(uint16_t) cicp.rho);
if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
{
heif_nclx_color_profile_free(nclx_profile);
- return(MagickFalse);
+ return((struct heif_color_profile_nclx *) NULL);
}
error=heif_nclx_color_profile_set_transfer_characteristics(nclx_profile,
(uint16_t) cicp.sigma);
if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
{
heif_nclx_color_profile_free(nclx_profile);
- return(MagickFalse);
+ return((struct heif_color_profile_nclx *) NULL);
}
error=heif_nclx_color_profile_set_matrix_coefficients(nclx_profile,
(uint16_t) cicp.xi);
if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
{
heif_nclx_color_profile_free(nclx_profile);
- return(MagickFalse);
+ return((struct heif_color_profile_nclx *) NULL);
}
nclx_profile->full_range_flag=(uint8_t) cicp.psi;
+ return(nclx_profile);
+}
+
+static MagickBooleanType SetHEICOutputCICPProfile(const ImageInfo *image_info,
+ Image *image,const char *chroma,MagickBooleanType image_list,
+ struct heif_color_profile_nclx **profile,ExceptionInfo *exception)
+{
+ Image
+ *p;
+
+ *profile=(struct heif_color_profile_nclx *) NULL;
+ if ((IsHEICSubsampledChroma(chroma) == MagickFalse) ||
+ (IsHEICExplicitCICP(image_info) != MagickFalse))
+ return(MagickTrue);
+ for (p=image; p != (Image *) NULL; p=GetNextImageInList(p))
+ {
+ const char
+ *option;
+
+ option=GetHEICCICPOption(image_info,p,exception);
+ if (IsHEICCICPIdentityMatrix(option) == MagickFalse)
+ {
+ if (image_list == MagickFalse)
+ break;
+ continue;
+ }
+ *profile=CreateHEICCICPProfile(p,option,MagickTrue,exception);
+ if (*profile == (struct heif_color_profile_nclx *) NULL)
+ return(MagickFalse);
+ return(MagickTrue);
+ }
+ return(MagickTrue);
+}
+
+static MagickBooleanType WriteHEICCICPProfile(Image *image,
+ struct heif_image *heif_image,const char *option,ExceptionInfo *exception)
+{
+ struct heif_color_profile_nclx
+ *nclx_profile;
+
+ struct heif_error
+ error;
+
+ nclx_profile=CreateHEICCICPProfile(image,option,MagickFalse,exception);
+ if (nclx_profile == (struct heif_color_profile_nclx *) NULL)
+ return(MagickFalse);
error=heif_image_set_nclx_color_profile(heif_image,nclx_profile);
heif_nclx_color_profile_free(nclx_profile);
return(IsHEIFSuccess(image,&error,exception));
@@ -1525,14 +1641,24 @@ static MagickBooleanType WriteHEICColorProperties(const ImageInfo *image_info,
*option;
#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
- option=GetImageOption(image_info,"heic:cicp");
- if ((option == (const char *) NULL) &&
- (GetImageProfile(image,"icc") == (const StringInfo *) NULL) &&
- (IsStringFalse(GetImageOption(image_info,"heic:preserve-cicp")) ==
- MagickFalse))
- option=GetImageProperty(image,"heic:cicp",exception);
+ option=GetHEICCICPOption(image_info,image,exception);
if (option != (const char *) NULL)
{
+ const char
+ *chroma;
+
+ chroma=GetImageOption(image_info,"heic:chroma");
+ if ((IsHEICExplicitCICP(image_info) != MagickFalse) &&
+ (IsHEICSubsampledChroma(chroma) != MagickFalse) &&
+ (IsHEICCICPIdentityMatrix(option) != MagickFalse))
+ {
+ (void) ThrowMagickException(exception,GetMagickModule(),
+ OptionError,"InvalidArgument","`heic:chroma=%s' conflicts with "
+ "identity matrix `heic:cicp=%s' for `%s'; use heic:chroma=444, "
+ "heic:preserve-cicp=false, or a non-identity heic:cicp value",
+ chroma,option,image->filename);
+ return(MagickFalse);
+ }
if (WriteHEICCICPProfile(image,heif_image,option,exception) ==
MagickFalse)
return(MagickFalse);
@@ -1859,6 +1985,9 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
struct heif_image
*heif_image = (struct heif_image *) NULL;
+ struct heif_color_profile_nclx
+ *output_nclx_profile = (struct heif_color_profile_nclx *) NULL;
+
struct heif_sequence_encoding_options
*seq_options = (struct heif_sequence_encoding_options *) NULL;
@@ -1903,6 +2032,10 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
if (option != (char *) NULL)
(void) heif_encoder_set_parameter(heif_encoder,"speed",option);
option=GetImageOption(image_info,"heic:chroma");
+ if ((option == (const char *) NULL) &&
+ (HEICImageHasIdentityCICP(image_info,image,MagickTrue,exception) !=
+ MagickFalse))
+ option="444";
if (option != (char *) NULL)
(void) heif_encoder_set_parameter(heif_encoder,"chroma",option);
/*
@@ -1923,10 +2056,23 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
ThrowWriterException(ImageError,"WidthOrHeightExceedsLimit");
}
seq_options=heif_sequence_encoding_options_alloc();
-#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,21,0)
if (seq_options != (struct heif_sequence_encoding_options *) NULL)
- seq_options->save_alpha_channel=1;
+ {
+ status=SetHEICOutputCICPProfile(image_info,image,option,MagickTrue,
+ &output_nclx_profile,exception);
+ if (status == MagickFalse)
+ {
+ heif_sequence_encoding_options_release(seq_options);
+ heif_encoder_release(heif_encoder);
+ heif_context_free(heif_context);
+ return(MagickFalse);
+ }
+ if (output_nclx_profile != (struct heif_color_profile_nclx *) NULL)
+ seq_options->output_nclx_profile=output_nclx_profile;
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,21,0)
+ seq_options->save_alpha_channel=1;
#endif
+ }
track_options=heif_track_options_alloc();
if (track_options != (struct heif_track_options *) NULL)
heif_track_options_set_timescale(track_options,timescale);
@@ -1940,6 +2086,8 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
{
if (seq_options != (struct heif_sequence_encoding_options *) NULL)
heif_sequence_encoding_options_release(seq_options);
+ if (output_nclx_profile != (struct heif_color_profile_nclx *) NULL)
+ heif_nclx_color_profile_free(output_nclx_profile);
heif_encoder_release(heif_encoder);
heif_context_free(heif_context);
return(MagickFalse);
@@ -2101,6 +2249,9 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
struct heif_image
*still_image;
+ struct heif_encoding_options
+ *still_options = (struct heif_encoding_options *) NULL;
+
first_image=GetFirstImageInList(image);
colorspace=heif_colorspace_YCbCr;
chroma=lossless != MagickFalse ? heif_chroma_444 : heif_chroma_420;
@@ -2145,13 +2296,35 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
status=WriteHEICImageRGBA(first_image,still_image,
exception);
}
+ if (status != MagickFalse)
+ {
+ if (output_nclx_profile !=
+ (struct heif_color_profile_nclx *) NULL)
+ {
+ still_options=heif_encoding_options_alloc();
+ if (still_options == (struct heif_encoding_options *)
+ NULL)
+ {
+ (void) ThrowMagickException(exception,
+ GetMagickModule(),ResourceLimitError,
+ "MemoryAllocationFailed","`%s'",
+ first_image->filename);
+ status=MagickFalse;
+ }
+ else
+ still_options->output_nclx_profile=
+ output_nclx_profile;
+ }
+ }
if (status != MagickFalse)
{
error=heif_context_encode_image(heif_context,still_image,
- heif_encoder,(struct heif_encoding_options *) NULL,
+ heif_encoder,still_options,
(struct heif_image_handle **) NULL);
status=IsHEIFSuccess(image,&error,exception);
}
+ if (still_options != (struct heif_encoding_options *) NULL)
+ heif_encoding_options_free(still_options);
heif_image_release(still_image);
}
}
@@ -2166,6 +2339,8 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
#endif
if (seq_options != (struct heif_sequence_encoding_options *) NULL)
heif_sequence_encoding_options_release(seq_options);
+ if (output_nclx_profile != (struct heif_color_profile_nclx *) NULL)
+ heif_nclx_color_profile_free(output_nclx_profile);
if (track != (heif_track *) NULL)
heif_track_release(track);
heif_encoder_release(heif_encoder);
@@ -2272,6 +2447,11 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
struct heif_encoding_options
*options;
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+ struct heif_color_profile_nclx
+ *output_nclx_profile = (struct heif_color_profile_nclx *) NULL;
+#endif
+
/*
Get encoder for the specified format.
*/
@@ -2360,6 +2540,12 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
break;
}
option=GetImageOption(image_info,"heic:chroma");
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+ if ((option == (const char *) NULL) &&
+ (HEICImageHasIdentityCICP(image_info,image,MagickFalse,exception) !=
+ MagickFalse))
+ option="444";
+#endif
if (option != (char *) NULL)
{
error=heif_encoder_set_parameter(heif_encoder,"chroma",option);
@@ -2368,6 +2554,24 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
break;
}
options=heif_encoding_options_alloc();
+ if (options == (struct heif_encoding_options *) NULL)
+ {
+ (void) ThrowMagickException(exception,GetMagickModule(),
+ ResourceLimitError,"MemoryAllocationFailed","`%s'",image->filename);
+ status=MagickFalse;
+ break;
+ }
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+ status=SetHEICOutputCICPProfile(image_info,image,option,MagickFalse,
+ &output_nclx_profile,exception);
+ if (status == MagickFalse)
+ {
+ heif_encoding_options_free(options);
+ break;
+ }
+ if (output_nclx_profile != (struct heif_color_profile_nclx *) NULL)
+ options->output_nclx_profile=output_nclx_profile;
+#endif
#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,16,0)
option=GetImageOption(image_info,"heic:chroma-downsampling");
if (option != (char *) NULL)
@@ -2405,6 +2609,10 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
error=heif_context_encode_image(heif_context,heif_image,heif_encoder,
options,(struct heif_image_handle **) NULL);
heif_encoding_options_free(options);
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+ if (output_nclx_profile != (struct heif_color_profile_nclx *) NULL)
+ heif_nclx_color_profile_free(output_nclx_profile);
+#endif
status=IsHEIFSuccess(image,&error,exception);
if (status == MagickFalse)
break;
diff --git a/tests/cli-heic.tap b/tests/cli-heic.tap
index 51002f4fe..2316393f4 100755
--- a/tests/cli-heic.tap
+++ b/tests/cli-heic.tap
@@ -25,17 +25,30 @@ no_cicp=heic_cicp_disabled.avif
no_clli=heic_clli_disabled.avif
invalid_clli=heic_clli_invalid.avif
malformed_clli=heic_clli_malformed.avif
+identity_source=heic_identity_cicp_source.avif
+identity_resized=heic_identity_cicp_resized.avif
+identity_explicit_chroma=heic_identity_cicp_chroma.avif
+identity_chroma_420=heic_identity_cicp_420.avif
+identity_chroma_422=heic_identity_cicp_422.avif
+identity_explicit_conflict=heic_identity_cicp_conflict.avif
+identity_no_cicp=heic_identity_cicp_disabled.avif
cleanup()
{
rm -f "$source" "$resized" "$explicit_cicp" "$explicit_clli" \
- "$no_cicp" "$no_clli" "$invalid_clli" "$malformed_clli"
+ "$no_cicp" "$no_clli" "$invalid_clli" "$malformed_clli" \
+ "$identity_source" "$identity_resized" "$identity_explicit_chroma" \
+ "$identity_chroma_420" "$identity_chroma_422" \
+ "$identity_explicit_conflict" "$identity_no_cicp"
}
check_property()
{
actual=`${IDENTIFY} -quiet -format "$2" "$1" 2>/dev/null`
- if [ "X$actual" = "X$3" ]; then
+ if [ $? -ne 0 ]; then
+ echo "not ok"
+ echo "# identify failed for '$1'"
+ elif [ "X$actual" = "X$3" ]; then
echo "ok"
else
echo "not ok"
@@ -59,7 +72,18 @@ if [ "X$metadata" != "X12/16/6/1 1624,182" ]; then
exit 0
fi
-echo "1..8"
+identity_cicp_supported=false
+if ${MAGICK} -size 4x4 gradient:red-blue -depth 10 \
+ -define heic:cicp=1/13/0/1 -define heic:chroma=444 \
+ "$identity_source" >/dev/null 2>&1; then
+ identity_metadata=`${IDENTIFY} -quiet -format '%[heic:cicp]' \
+ "$identity_source" 2>/dev/null`
+ if [ "X$identity_metadata" = "X1/13/0/1" ]; then
+ identity_cicp_supported=true
+ fi
+fi
+
+echo "1..14"
check_property "$source" '%[heic:cicp] %[heic:clli]' '12/16/6/1 1624,182'
@@ -100,5 +124,46 @@ else
echo "ok"
fi
+if [ "X$identity_cicp_supported" = "Xtrue" ]; then
+ ${MAGICK} "$identity_source" -resize 50% "$identity_resized" \
+ >/dev/null 2>&1 &&
+ check_property "$identity_resized" '%[heic:cicp]' '1/13/0/1' ||
+ echo "not ok"
+
+ ${MAGICK} "$identity_source" -resize 50% -define heic:chroma=444 \
+ "$identity_explicit_chroma" >/dev/null 2>&1 &&
+ check_property "$identity_explicit_chroma" '%[heic:cicp]' '1/13/0/1' ||
+ echo "not ok"
+
+ ${MAGICK} "$identity_source" -resize 50% -define heic:chroma=420 \
+ "$identity_chroma_420" >/dev/null 2>&1 &&
+ check_property "$identity_chroma_420" '%[heic:cicp]' '1/13/6/1' ||
+ echo "not ok"
+
+ ${MAGICK} "$identity_source" -resize 50% -define heic:chroma=422 \
+ "$identity_chroma_422" >/dev/null 2>&1 &&
+ check_property "$identity_chroma_422" '%[heic:cicp]' '1/13/6/1' ||
+ echo "not ok"
+
+ if ${MAGICK} "$identity_source" -resize 50% -define heic:cicp=1/13/0/1 \
+ -define heic:chroma=420 "$identity_explicit_conflict" \
+ >/dev/null 2>&1; then
+ echo "not ok"
+ else
+ echo "ok"
+ fi
+
+ ${MAGICK} "$identity_source" -resize 50% -define heic:preserve-cicp=false \
+ -define heic:chroma=420 "$identity_no_cicp" >/dev/null 2>&1 &&
+ check_property "$identity_no_cicp" '%[heic:cicp]' '' || echo "not ok"
+else
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+ echo "ok # SKIP HEIC identity matrix CICP unsupported by libheif delegate"
+fi
+
cleanup
:
diff --git a/www/defines.html b/www/defines.html
index 2c17285ad..8b5051f6d 100644
--- a/www/defines.html
+++ b/www/defines.html
@@ -977,12 +977,12 @@ use:</p>
<tr>
<td>heic:chroma=<var>value</var></td>
- <td>Set the HEIC chroma parameter. Possible values are: "420", "422", "444". Default is "420".</td>
+ <td>Set the HEIC chroma parameter. Possible values are: "420", "422", "444". Default is "420", except output with preserved CICP matrix coefficient 0 uses "444" when this define is omitted because identity matrix CICP cannot be written with chroma subsampling.</td>
</tr>
<tr>
<td>heic:cicp=<var>value</var></td>
- <td>Set the HEIC color primaries, transfer characteristics, matrix coefficients, and full range flag. Use <samp>1/13/6/1</samp> for full range BT.709. If omitted, the source HEIC CICP value is preserved when available unless <samp>heic:preserve-cicp=false</samp> is set. Requires libheif 1.17.0 or later. See ISO/IEC 14496-12:2022 standard for a description of these fields and values.</td>
+ <td>Set the HEIC color primaries, transfer characteristics, matrix coefficients, and full range flag. Use <samp>1/13/6/1</samp> for full range BT.709. If omitted, the source HEIC CICP value is preserved when available unless <samp>heic:preserve-cicp=false</samp> is set. When preserving source matrix coefficient 0 with <samp>heic:chroma=420</samp> or <samp>422</samp>, ImageMagick writes matrix coefficient 6 (BT.601) for the subsampled YCbCr output. Explicit <samp>heic:cicp</samp> values with matrix coefficient 0 require <samp>heic:chroma=444</samp>. Requires libheif 1.17.0 or later. See ISO/IEC 14496-12:2022 standard for a description of these fields and values.</td>
</tr>
<tr>