Commit b6f4e3112 for imagemagick.org

commit b6f4e3112eb7ee82f7313c4ecefaaf621329603f
Author: Greg B <64932474+gregbenz@users.noreply.github.com>
Date:   Sat Jun 27 06:32:59 2026 -0500

    Preserve HEIC CICP and CLLI metadata (#8824)

    Co-authored-by: Greg Benz <git@gregbenzphotography.com>

diff --git a/Makefile.in b/Makefile.in
index 9fd4c5313..eb9d7dc3e 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -6195,6 +6195,7 @@ tests_wandtest_LDADD = $(MAGICKCORE_LIBS) $(MAGICKWAND_LIBS)
 TESTS_XFAIL_TESTS =
 TESTS_TESTS = \
   tests/cli-colorspace.tap \
+  tests/cli-heic.tap \
   tests/cli-pipe.tap \
   tests/cli-svg.tap \
   tests/validate-colorspace.tap \
diff --git a/coders/heic.c b/coders/heic.c
index eb6a99bfd..54ba83521 100644
--- a/coders/heic.c
+++ b/coders/heic.c
@@ -208,6 +208,61 @@ static inline MagickBooleanType IsHEIFSuccess(Image *image,
   return(MagickFalse);
 }

+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+static void ReadHEICCICPProfile(Image *image,
+  struct heif_image_handle *image_handle,ExceptionInfo *exception)
+{
+  char
+    property[MagickPathExtent];
+
+  struct heif_color_profile_nclx
+    *nclx_profile;
+
+  struct heif_error
+    error;
+
+  nclx_profile=(struct heif_color_profile_nclx *) NULL;
+  error=heif_image_handle_get_nclx_color_profile(image_handle,&nclx_profile);
+  if (error.code == heif_error_Color_profile_does_not_exist)
+    return;
+  if (IsHEIFSuccess(image,&error,exception) == MagickFalse)
+    {
+      if (nclx_profile != (struct heif_color_profile_nclx *) NULL)
+        heif_nclx_color_profile_free(nclx_profile);
+      return;
+    }
+  if (nclx_profile == (struct heif_color_profile_nclx *) NULL)
+    return;
+  (void) FormatLocaleString(property,MagickPathExtent,"%u/%u/%u/%u",
+    (unsigned int) nclx_profile->color_primaries,(unsigned int)
+    nclx_profile->transfer_characteristics,(unsigned int)
+    nclx_profile->matrix_coefficients,(unsigned int)
+    nclx_profile->full_range_flag);
+  (void) SetImageProperty(image,"heic:cicp",property,exception);
+  heif_nclx_color_profile_free(nclx_profile);
+}
+#endif
+
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+static void ReadHEICContentLightLevel(Image *image,
+  struct heif_image_handle *image_handle,ExceptionInfo *exception)
+{
+  char
+    property[MagickPathExtent];
+
+  struct heif_content_light_level
+    content_light_level;
+
+  if (heif_image_handle_get_content_light_level(image_handle,
+      &content_light_level) == 0)
+    return;
+  (void) FormatLocaleString(property,MagickPathExtent,"%u,%u",(unsigned int)
+    content_light_level.max_content_light_level,(unsigned int)
+    content_light_level.max_pic_average_light_level);
+  (void) SetImageProperty(image,"heic:clli",property,exception);
+}
+#endif
+
 static MagickBooleanType ReadHEICColorProfile(Image *image,
   struct heif_image_handle *image_handle,ExceptionInfo *exception)
 {
@@ -494,6 +549,12 @@ static MagickBooleanType ReadHEICImageHandle(const ImageInfo *image_info,
     }
   if (ReadHEICColorProfile(image,image_handle,exception) == MagickFalse)
     return(MagickFalse);
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
+  ReadHEICCICPProfile(image,image_handle,exception);
+#endif
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+  ReadHEICContentLightLevel(image,image_handle,exception);
+#endif
   if (ReadHEICExifProfile(image,image_handle,exception) == MagickFalse)
     return(MagickFalse);
   if (ReadHEICXMPProfile(image,image_handle,exception) == MagickFalse)
@@ -1345,6 +1406,158 @@ static void WriteProfile(struct heif_context *context,Image *image,
   heif_image_handle_release(image_handle);
 }

+#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)
+{
+  GeometryInfo
+    cicp;
+
+  struct heif_color_profile_nclx
+    *nclx_profile;
+
+  struct heif_error
+    error;
+
+  SetGeometryInfo(&cicp);
+  nclx_profile=heif_nclx_color_profile_alloc();
+  if (nclx_profile == (struct heif_color_profile_nclx *) NULL)
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),
+        ResourceLimitError,"MemoryAllocationFailed","`%s'",image->filename);
+      return(MagickFalse);
+    }
+  cicp.rho=(double) nclx_profile->color_primaries;
+  cicp.sigma=(double) nclx_profile->transfer_characteristics;
+  cicp.xi=(double) nclx_profile->matrix_coefficients;
+  cicp.psi=(double) nclx_profile->full_range_flag;
+  (void) ParseGeometry(option,&cicp);
+  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);
+    }
+  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);
+    }
+  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);
+    }
+  nclx_profile->full_range_flag=(uint8_t) cicp.psi;
+  error=heif_image_set_nclx_color_profile(heif_image,nclx_profile);
+  heif_nclx_color_profile_free(nclx_profile);
+  return(IsHEIFSuccess(image,&error,exception));
+}
+#endif
+
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+static MagickBooleanType WriteHEICContentLightLevel(Image *image,
+  struct heif_image *heif_image,const char *option,ExceptionInfo *exception)
+{
+  char
+    *p,
+    *q;
+
+  struct heif_content_light_level
+    content_light_level;
+
+  unsigned long
+    max_content_light_level,
+    max_pic_average_light_level;
+
+  errno=0;
+  max_content_light_level=strtoul(option,&q,10);
+  if ((errno != 0) || (q == option) ||
+      (max_content_light_level > (unsigned long) UINT16_MAX))
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),OptionError,
+        "InvalidArgument","`heic:clli=%s' for `%s'",option,image->filename);
+      return(MagickFalse);
+    }
+  while (isspace((int) ((unsigned char) *q)) != 0)
+    q++;
+  if (*q != ',')
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),OptionError,
+        "InvalidArgument","`heic:clli=%s' for `%s'",option,image->filename);
+      return(MagickFalse);
+    }
+  q++;
+  while (isspace((int) ((unsigned char) *q)) != 0)
+    q++;
+  p=q;
+  errno=0;
+  max_pic_average_light_level=strtoul(q,&q,10);
+  if ((errno != 0) || (q == p) || (max_pic_average_light_level >
+      (unsigned long) UINT16_MAX))
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),OptionError,
+        "InvalidArgument","`heic:clli=%s' for `%s'",option,image->filename);
+      return(MagickFalse);
+    }
+  while (isspace((int) ((unsigned char) *q)) != 0)
+    q++;
+  if (*q != '\0')
+    {
+      (void) ThrowMagickException(exception,GetMagickModule(),OptionError,
+        "InvalidArgument","`heic:clli=%s' for `%s'",option,image->filename);
+      return(MagickFalse);
+    }
+  content_light_level.max_content_light_level=(uint16_t)
+    max_content_light_level;
+  content_light_level.max_pic_average_light_level=(uint16_t)
+    max_pic_average_light_level;
+  heif_image_set_content_light_level(heif_image,&content_light_level);
+  return(MagickTrue);
+}
+#endif
+
+static MagickBooleanType WriteHEICColorProperties(const ImageInfo *image_info,
+  Image *image,struct heif_image *heif_image,ExceptionInfo *exception)
+{
+  const char
+    *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);
+  if (option != (const char *) NULL)
+    {
+      if (WriteHEICCICPProfile(image,heif_image,option,exception) ==
+          MagickFalse)
+        return(MagickFalse);
+    }
+#endif
+#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,20,0)
+  option=GetImageOption(image_info,"heic:clli");
+  if ((option == (const char *) NULL) &&
+      (IsStringFalse(GetImageOption(image_info,"heic:preserve-clli")) ==
+       MagickFalse))
+    option=GetImageProperty(image,"heic:clli",exception);
+  if (option != (const char *) NULL)
+    {
+      if (WriteHEICContentLightLevel(image,heif_image,option,exception) ==
+          MagickFalse)
+        return(MagickFalse);
+    }
+#endif
+  return(MagickTrue);
+}
+
 static struct heif_error heif_write_func(struct heif_context *context,
   const void* data,size_t size,void* userdata)
 {
@@ -1814,6 +2027,13 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
     status=IsHEIFSuccess(image,&error,exception);
     if (status == MagickFalse)
       break;
+    status=WriteHEICColorProperties(image_info,image,heif_image,exception);
+    if (status == MagickFalse)
+      {
+        heif_image_release(heif_image);
+        heif_image=(struct heif_image *) NULL;
+        break;
+      }
     profile=GetImageProfile(image,"icc");
     if (profile != (StringInfo *) NULL)
       (void) heif_image_set_raw_color_profile(heif_image,"prof",
@@ -1914,14 +2134,21 @@ static MagickBooleanType WriteHEICSequenceImage(const ImageInfo *image_info,
             (int) first_image->rows,colorspace,chroma,&still_image);
           if (IsHEIFSuccess(image,&error,exception) != MagickFalse)
             {
-              if (colorspace == heif_colorspace_YCbCr)
-                status=WriteHEICImageYCbCr(first_image,still_image,exception);
-              else
-                if (first_image->depth > 8)
-                  status=WriteHEICImageRRGGBBAA(first_image,still_image,
-                    exception);
-                else
-                  status=WriteHEICImageRGBA(first_image,still_image,exception);
+              status=WriteHEICColorProperties(image_info,first_image,
+                still_image,exception);
+              if (status != MagickFalse)
+                {
+                  if (colorspace == heif_colorspace_YCbCr)
+                    status=WriteHEICImageYCbCr(first_image,still_image,
+                      exception);
+                  else
+                    if (first_image->depth > 8)
+                      status=WriteHEICImageRRGGBBAA(first_image,still_image,
+                        exception);
+                    else
+                      status=WriteHEICImageRGBA(first_image,still_image,
+                        exception);
+                }
               if (status != MagickFalse)
                 {
                   error=heif_context_encode_image(heif_context,still_image,
@@ -2101,36 +2328,9 @@ static MagickBooleanType WriteHEICImage(const ImageInfo *image_info,
     status=IsHEIFSuccess(image,&error,exception);
     if (status == MagickFalse)
       break;
-#if LIBHEIF_NUMERIC_VERSION >= HEIC_COMPUTE_NUMERIC_VERSION(1,17,0)
-    option=GetImageOption(image_info,"heic:cicp");
-    if (option != (char *) NULL)
-      {
-        GeometryInfo
-          cicp;
-
-        struct heif_color_profile_nclx
-          *nclx_profile;
-
-        SetGeometryInfo(&cicp);
-        nclx_profile=heif_nclx_color_profile_alloc();
-        if (nclx_profile == (struct heif_color_profile_nclx *) NULL)
-          ThrowWriterException(ResourceLimitError,"MemoryAllocationFailed");
-        cicp.rho=(double) nclx_profile->color_primaries;
-        cicp.sigma=(double) nclx_profile->transfer_characteristics;
-        cicp.xi=(double) nclx_profile->matrix_coefficients;
-        cicp.psi=(double) nclx_profile->full_range_flag;
-        (void) ParseGeometry(option,&cicp);
-        heif_nclx_color_profile_set_color_primaries(nclx_profile,
-          (uint16_t) cicp.rho);
-        heif_nclx_color_profile_set_transfer_characteristics(nclx_profile,
-          (uint16_t) cicp.sigma);
-        heif_nclx_color_profile_set_matrix_coefficients(nclx_profile,
-          (uint16_t) cicp.xi);
-        nclx_profile->full_range_flag=(uint8_t) cicp.psi;
-        heif_image_set_nclx_color_profile(heif_image,nclx_profile);
-        heif_nclx_color_profile_free(nclx_profile);
-      }
-#endif
+    status=WriteHEICColorProperties(image_info,image,heif_image,exception);
+    if (status == MagickFalse)
+      break;
     profile=GetImageProfile(image,"icc");
     if (profile != (StringInfo *) NULL)
       (void) heif_image_set_raw_color_profile(heif_image,"prof",
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b20e279d5..40b019a31 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -39,6 +39,7 @@ TESTS_XFAIL_TESTS =

 TESTS_TESTS = \
   tests/cli-colorspace.tap \
+  tests/cli-heic.tap \
   tests/cli-pipe.tap \
   tests/cli-svg.tap \
   tests/validate-colorspace.tap \
diff --git a/tests/cli-heic.tap b/tests/cli-heic.tap
new file mode 100755
index 000000000..51002f4fe
--- /dev/null
+++ b/tests/cli-heic.tap
@@ -0,0 +1,104 @@
+#!/bin/sh
+#
+#  Copyright 1999 ImageMagick Studio LLC, a non-profit organization
+#  dedicated to making software imaging solutions freely available.
+#
+#  You may not use this file except in compliance with the License.  You may
+#  obtain a copy of the License at
+#
+#    https://imagemagick.org/license/
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#
+. ./common.shi
+. ${srcdir}/tests/common.shi
+
+source=heic_cicp_source.avif
+resized=heic_cicp_resized.avif
+explicit_cicp=heic_cicp_explicit.avif
+explicit_clli=heic_clli_explicit.avif
+no_cicp=heic_cicp_disabled.avif
+no_clli=heic_clli_disabled.avif
+invalid_clli=heic_clli_invalid.avif
+malformed_clli=heic_clli_malformed.avif
+
+cleanup()
+{
+  rm -f "$source" "$resized" "$explicit_cicp" "$explicit_clli" \
+    "$no_cicp" "$no_clli" "$invalid_clli" "$malformed_clli"
+}
+
+check_property()
+{
+  actual=`${IDENTIFY} -quiet -format "$2" "$1" 2>/dev/null`
+  if [ "X$actual" = "X$3" ]; then
+    echo "ok"
+  else
+    echo "not ok"
+    echo "# expected '$3', got '$actual'"
+  fi
+}
+
+cleanup
+if ! ${MAGICK} -size 4x4 gradient:red-blue -depth 10 \
+    -define heic:cicp=12/16/6/1 -define heic:clli=1624,182 \
+    "$source" >/dev/null 2>&1; then
+  echo "1..0 # SKIP AVIF coder unavailable"
+  exit 0
+fi
+
+metadata=`${IDENTIFY} -quiet -format '%[heic:cicp] %[heic:clli]' \
+  "$source" 2>/dev/null`
+if [ "X$metadata" != "X12/16/6/1 1624,182" ]; then
+  echo "1..0 # SKIP HEIC CICP/CLLI unsupported by libheif delegate"
+  cleanup
+  exit 0
+fi
+
+echo "1..8"
+
+check_property "$source" '%[heic:cicp] %[heic:clli]' '12/16/6/1 1624,182'
+
+${MAGICK} "$source" -resize 50% "$resized" >/dev/null 2>&1 &&
+  check_property "$resized" '%[heic:cicp] %[heic:clli]' \
+    '12/16/6/1 1624,182' || echo "not ok"
+
+${MAGICK} "$source" -resize 50% -define heic:cicp=1/13/6/1 \
+  "$explicit_cicp" >/dev/null 2>&1 &&
+  check_property "$explicit_cicp" '%[heic:cicp] %[heic:clli]' \
+    '1/13/6/1 1624,182' || echo "not ok"
+
+${MAGICK} "$source" -resize 50% -define heic:clli='1000, 200' \
+  "$explicit_clli" >/dev/null 2>&1 &&
+  check_property "$explicit_clli" '%[heic:cicp] %[heic:clli]' \
+    '12/16/6/1 1000,200' || echo "not ok"
+
+${MAGICK} "$source" -resize 50% -define heic:preserve-cicp=false \
+  "$no_cicp" >/dev/null 2>&1 &&
+  check_property "$no_cicp" '%[heic:cicp]' '' || echo "not ok"
+
+${MAGICK} "$source" -resize 50% -define heic:preserve-clli=false \
+  "$no_clli" >/dev/null 2>&1 &&
+  check_property "$no_clli" '%[heic:cicp] %[heic:clli]' '12/16/6/1 ' ||
+  echo "not ok"
+
+if ${MAGICK} "$source" -define heic:clli=70000,200 "$invalid_clli" \
+    >/dev/null 2>&1; then
+  echo "not ok"
+else
+  echo "ok"
+fi
+
+if ${MAGICK} "$source" -define heic:clli=1000,200x "$malformed_clli" \
+    >/dev/null 2>&1; then
+  echo "not ok"
+else
+  echo "ok"
+fi
+
+cleanup
+:
diff --git a/www/defines.html b/www/defines.html
index ccf4b602f..2c17285ad 100644
--- a/www/defines.html
+++ b/www/defines.html
@@ -982,7 +982,12 @@ use:</p>

   <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.  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. 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>
+    <td>heic:clli=<var>value</var></td>
+    <td>Set the HEIC content light level information as <samp>MaxCLL,MaxFALL</samp>. If omitted, the source HEIC content light level is preserved when available unless <samp>heic:preserve-clli=false</samp> is set. Requires libheif 1.20.0 or later.</td>
   </tr>

   <tr>
@@ -1033,6 +1038,16 @@ use:</p>
     </td>
   </tr>

+  <tr>
+    <td>heic:preserve-cicp=<var>false</var></td>
+    <td>Do not preserve the source HEIC CICP color information when writing HEIC output. Requires libheif 1.17.0 or later.</td>
+  </tr>
+
+  <tr>
+    <td>heic:preserve-clli=<var>false</var></td>
+    <td>Do not preserve the source HEIC content light level information when writing HEIC output. Requires libheif 1.20.0 or later.</td>
+  </tr>
+
   <tr>
     <td>heic:speed=<var>value</var></td>
     <td>Set the HEIC speed parameter. Integer value from 0-9. Default is 5.</td>
@@ -1951,4 +1966,4 @@ use:</p>
   <script src="assets/bootstrap.bundle.min.js" ></script>
   </body>
 </html>
-<!-- Magick Cache 27th July 2025 09:38 -->
\ No newline at end of file
+<!-- Magick Cache 27th July 2025 09:38 -->
diff --git a/www/formats.html b/www/formats.html
index 4066a6d9f..a6f2a14c8 100644
--- a/www/formats.html
+++ b/www/formats.html
@@ -679,7 +679,7 @@ the supported image formats.</p>
     <td><a href="https://en.wikipedia.org/wiki/High_Efficiency_Image_File_Format">HEIC</a></td>
     <td>RW</td>
     <td>Apple High efficiency Image Format</td>
-    <td>Set the quality to 100 to produce lossless HEIC images.  Requires the <a href="https://github.com/strukturag/libheif">libheif</a> delegate library.  Recognized defines include <samp>heic:cicp</samp>, <samp>heic:preserve-orientation={true,false}</samp>, <samp>depth-image={true,false}</samp>, <samp>heic:speed</samp>, and <samp>heic:chroma</samp>.</td>
+    <td>Set the quality to 100 to produce lossless HEIC images.  Requires the <a href="https://github.com/strukturag/libheif">libheif</a> delegate library.  Recognized defines include <samp>heic:cicp</samp>, <samp>heic:clli</samp>, <samp>heic:preserve-cicp={true,false}</samp>, <samp>heic:preserve-clli={true,false}</samp>, <samp>heic:preserve-orientation={true,false}</samp>, <samp>depth-image={true,false}</samp>, <samp>heic:speed</samp>, and <samp>heic:chroma</samp>; availability depends on the libheif delegate version.</td>
   </tr>

   <tr>
@@ -2332,4 +2332,4 @@ the supported image formats.</p>
 ~
   </body>
 </html>
-<!-- Magick Cache 3rd October 2025 21:07 -->
\ No newline at end of file
+<!-- Magick Cache 3rd October 2025 21:07 -->