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>