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 -->