Set Unspecified CICP in Seq Header OBU (#2687)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e7f786a..0b390f0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,8 @@
 * Update aom.cmd/LocalAom.cmake: v3.12.0-4-g65ddc22823. This fixes an encode &
   decode issue on PowerPC (ppc64le).
 * Change avifenc to start in automatic tiling mode.
+* Always forward Unspecified (2) CICP color primaries, transfer characteristics,
+  and matrix coefficients to the AV1 encoder. Rely on the 'colr' box instead.
 
 ## [1.2.1] - 2025-03-17
 
diff --git a/src/codec_aom.c b/src/codec_aom.c
index 2dc5f97..e472736 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -838,26 +838,41 @@
 
         // Set color_config() in the sequence header OBU.
         if (alpha) {
+            // AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+            //   The color_range field in the Sequence Header OBU shall be set to 1.
             aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, AOM_CR_FULL_RANGE);
+
+            // Keep the default AOM_CSP_UNKNOWN value.
+
+            // CICP (CP/TC/MC) does not apply to the alpha auxiliary image.
+            // Keep default Unspecified (2) colour primaries, transfer characteristics,
+            // and matrix coefficients.
         } else {
-            // libaom's defaults are AOM_CICP_CP_UNSPECIFIED, AOM_CICP_TC_UNSPECIFIED,
-            // AOM_CICP_MC_UNSPECIFIED, AOM_CSP_UNKNOWN, and 0 (studio/limited range). Call
-            // aom_codec_control() only if the values are not the defaults.
-            if (image->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_PRIMARIES, (int)image->colorPrimaries);
-            }
-            if (image->transferCharacteristics != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_TRANSFER_CHARACTERISTICS, (int)image->transferCharacteristics);
-            }
-            if (image->matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_MATRIX_COEFFICIENTS, (int)image->matrixCoefficients);
-            }
+            // libaom's defaults are AOM_CSP_UNKNOWN and 0 (studio/limited range).
+            // Call aom_codec_control() only if the values are not the defaults.
+
+            // AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+            //   The values of the fields in the AV1CodecConfigurationBox shall match those
+            //   of the Sequence Header OBU in the AV1 Image Item Data.
             if (image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN) {
                 aom_codec_control(&codec->internal->encoder, AV1E_SET_CHROMA_SAMPLE_POSITION, (int)image->yuvChromaSamplePosition);
             }
+
+            // AV1-ISOBMFF specification, Section 2.3.4:
+            //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+            //   flag in the Sequence Header OBU.
             if (image->yuvRange != AVIF_RANGE_LIMITED) {
                 aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, (int)image->yuvRange);
             }
+
+            // Section 2.3.4 of AV1-ISOBMFF says 'colr' with 'nclx' should be present and shall match CICP
+            // values in the Sequence Header OBU, unless the latter has 2/2/2 (Unspecified).
+            // So set CICP values to 2/2/2 (Unspecified) in the Sequence Header OBU for simplicity.
+            // It may also save 3 bytes since the AV1 encoder can set color_description_present_flag to 0
+            // (see Section 5.5.2 "Color config syntax" of the AV1 specification).
+            // libaom's defaults are AOM_CICP_CP_UNSPECIFIED, AOM_CICP_TC_UNSPECIFIED, and
+            // AOM_CICP_MC_UNSPECIFIED. No need to call aom_codec_control().
+            // aom_image_t::cp, aom_image_t::tc and aom_image_t::mc are ignored by aom_codec_encode().
         }
 
 #if defined(AOM_CTRL_AV1E_SET_SKIP_POSTPROC_FILTERING)
@@ -1032,7 +1047,13 @@
     avifBool monochromeRequested = AVIF_FALSE;
 
     if (alpha) {
+        // AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The color_range field in the Sequence Header OBU shall be set to 1.
         aomImage.range = AOM_CR_FULL_RANGE;
+
+        // AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The mono_chrome field in the Sequence Header OBU shall be set to 1.
+        // Some encoders do not support 4:0:0 and encode alpha as 4:2:0 so it is not always respected.
         monochromeRequested = AVIF_TRUE;
         if (aomImageAllocated) {
             const uint32_t bytesPerRow = ((image->depth > 8) ? 2 : 1) * image->width;
@@ -1046,7 +1067,7 @@
             aomImage.stride[0] = image->alphaRowBytes;
         }
 
-        // Ignore UV planes when monochrome
+        // Ignore UV planes when monochrome. Keep the default AOM_CSP_UNKNOWN value.
     } else {
         int yuvPlaneCount = 3;
         if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
@@ -1073,10 +1094,14 @@
             }
         }
 
-        aomImage.cp = (aom_color_primaries_t)image->colorPrimaries;
-        aomImage.tc = (aom_transfer_characteristics_t)image->transferCharacteristics;
-        aomImage.mc = (aom_matrix_coefficients_t)image->matrixCoefficients;
+        // AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+        //   The values of the fields in the AV1CodecConfigurationBox shall match those
+        //   of the Sequence Header OBU in the AV1 Image Item Data.
         aomImage.csp = (aom_chroma_sample_position_t)image->yuvChromaSamplePosition;
+
+        // AV1-ISOBMFF specification, Section 2.3.4:
+        //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+        //   flag in the Sequence Header OBU.
         aomImage.range = (aom_color_range_t)image->yuvRange;
     }
 
diff --git a/src/codec_avm.c b/src/codec_avm.c
index 02cebe6..b8295e1 100644
--- a/src/codec_avm.c
+++ b/src/codec_avm.c
@@ -625,26 +625,41 @@
 
         // Set color_config() in the sequence header OBU.
         if (alpha) {
+            // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+            //   The color_range field in the Sequence Header OBU shall be set to 1.
             aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, AOM_CR_FULL_RANGE);
+
+            // Keep the default AOM_CSP_UNKNOWN value.
+
+            // CICP (CP/TC/MC) does not apply to the alpha auxiliary image.
+            // Keep default Unspecified (2) colour primaries, transfer characteristics,
+            // and matrix coefficients.
         } else {
-            // libavm's defaults are AOM_CICP_CP_UNSPECIFIED, AOM_CICP_TC_UNSPECIFIED,
-            // AOM_CICP_MC_UNSPECIFIED, AOM_CSP_UNKNOWN, and 0 (studio/limited range). Call
-            // aom_codec_control() only if the values are not the defaults.
-            if (image->colorPrimaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_PRIMARIES, (int)image->colorPrimaries);
-            }
-            if (image->transferCharacteristics != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_TRANSFER_CHARACTERISTICS, (int)image->transferCharacteristics);
-            }
-            if (image->matrixCoefficients != AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
-                aom_codec_control(&codec->internal->encoder, AV1E_SET_MATRIX_COEFFICIENTS, (int)image->matrixCoefficients);
-            }
+            // libaom's defaults are AOM_CSP_UNKNOWN and 0 (studio/limited range).
+            // Call aom_codec_control() only if the values are not the defaults.
+
+            // AV1-AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+            //   The values of the fields in the AV1CodecConfigurationBox shall match those
+            //   of the Sequence Header OBU in the AV1 Image Item Data.
             if (image->yuvChromaSamplePosition != AVIF_CHROMA_SAMPLE_POSITION_UNKNOWN) {
                 aom_codec_control(&codec->internal->encoder, AV1E_SET_CHROMA_SAMPLE_POSITION, (int)image->yuvChromaSamplePosition);
             }
+
+            // AV1-ISOBMFF specification, Section 2.3.4:
+            //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+            //   flag in the Sequence Header OBU.
             if (image->yuvRange != AVIF_RANGE_LIMITED) {
                 aom_codec_control(&codec->internal->encoder, AV1E_SET_COLOR_RANGE, (int)image->yuvRange);
             }
+
+            // Section 2.3.4 of AV1-ISOBMFF says 'colr' with 'nclx' should be present and shall match CICP
+            // values in the Sequence Header OBU, unless the latter has 2/2/2 (Unspecified).
+            // So set CICP values to 2/2/2 (Unspecified) in the Sequence Header OBU for simplicity.
+            // It may also save 3 bytes since the AV1 encoder can set color_description_present_flag to 0
+            // (see Section 5.5.2 "Color config syntax" of the AV1 specification).
+            // libaom's defaults are AOM_CICP_CP_UNSPECIFIED, AOM_CICP_TC_UNSPECIFIED, and
+            // AOM_CICP_MC_UNSPECIFIED. No need to call aom_codec_control().
+            // aom_image_t::cp, aom_image_t::tc and aom_image_t::mc are ignored by aom_codec_encode().
         }
 
         if (!avifProcessAOMOptionsPostInit(codec, alpha)) {
@@ -810,7 +825,13 @@
     avifBool monochromeRequested = AVIF_FALSE;
 
     if (alpha) {
+        // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The color_range field in the Sequence Header OBU shall be set to 1.
         aomImage.range = AOM_CR_FULL_RANGE;
+
+        // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The mono_chrome field in the Sequence Header OBU shall be set to 1.
+        // Some encoders do not support 4:0:0 and encode alpha as 4:2:0 so it is not always respected.
         monochromeRequested = AVIF_TRUE;
         if (aomImageAllocated) {
             const uint32_t bytesPerRow = ((image->depth > 8) ? 2 : 1) * image->width;
@@ -824,7 +845,7 @@
             aomImage.stride[0] = image->alphaRowBytes;
         }
 
-        // Ignore UV planes when monochrome
+        // Ignore UV planes when monochrome. Keep the default AOM_CSP_UNKNOWN value.
     } else {
         int yuvPlaneCount = 3;
         if (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) {
@@ -851,10 +872,14 @@
             }
         }
 
-        aomImage.cp = (aom_color_primaries_t)image->colorPrimaries;
-        aomImage.tc = (aom_transfer_characteristics_t)image->transferCharacteristics;
-        aomImage.mc = (aom_matrix_coefficients_t)image->matrixCoefficients;
+        // AV1-AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+        //   The values of the fields in the AV1CodecConfigurationBox shall match those
+        //   of the Sequence Header OBU in the AV1 Image Item Data.
         aomImage.csp = (aom_chroma_sample_position_t)image->yuvChromaSamplePosition;
+
+        // AV1-ISOBMFF specification, Section 2.3.4:
+        //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+        //   flag in the Sequence Header OBU.
         aomImage.range = (aom_color_range_t)image->yuvRange;
     }
 
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index c39c9a6..293d100 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -91,11 +91,27 @@
         const avifBool supports400 = rav1eSupports400();
         RaPixelRange rav1eRange;
         if (alpha) {
+            // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+            //   The color_range field in the Sequence Header OBU shall be set to 1.
             rav1eRange = RA_PIXEL_RANGE_FULL;
+
+            // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+            //   The mono_chrome field in the Sequence Header OBU shall be set to 1.
+            // Some encoders do not support 4:0:0 and encode alpha as 4:2:0 so it is not always respected.
             codec->internal->chromaSampling = supports400 ? RA_CHROMA_SAMPLING_CS400 : RA_CHROMA_SAMPLING_CS420;
             codec->internal->yShift = 1;
+
+            // CICP (CP/TC/MC) does not apply to the alpha auxiliary image.
+            // Use Unspecified (2) colour primaries, transfer characteristics, and matrix coefficients below.
         } else {
+            // AV1-ISOBMFF specification, Section 2.3.4:
+            //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+            //   flag in the Sequence Header OBU.
             rav1eRange = (image->yuvRange == AVIF_RANGE_FULL) ? RA_PIXEL_RANGE_FULL : RA_PIXEL_RANGE_LIMITED;
+
+            // AV1-AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+            //   The values of the fields in the AV1CodecConfigurationBox shall match those
+            //   of the Sequence Header OBU in the AV1 Image Item Data.
             codec->internal->yShift = 0;
             switch (image->yuvFormat) {
                 case AVIF_PIXEL_FORMAT_YUV444:
@@ -186,10 +202,15 @@
             }
         }
 
+        // Section 2.3.4 of AV1-ISOBMFF says 'colr' with 'nclx' should be present and shall match CICP
+        // values in the Sequence Header OBU, unless the latter has 2/2/2 (Unspecified).
+        // So set CICP values to 2/2/2 (Unspecified) in the Sequence Header OBU for simplicity.
+        // It may also save 3 bytes since the AV1 encoder may set color_description_present_flag to 0
+        // (see Section 5.5.2 "Color config syntax" of the AV1 specification).
         rav1e_config_set_color_description(rav1eConfig,
-                                           (RaMatrixCoefficients)image->matrixCoefficients,
-                                           (RaColorPrimaries)image->colorPrimaries,
-                                           (RaTransferCharacteristics)image->transferCharacteristics);
+                                           RA_MATRIX_COEFFICIENTS_UNSPECIFIED,
+                                           RA_COLOR_PRIMARIES_UNSPECIFIED,
+                                           RA_TRANSFER_CHARACTERISTICS_UNSPECIFIED);
 
         codec->internal->rav1eContext = rav1e_context_new(rav1eConfig);
         if (!codec->internal->rav1eContext) {
diff --git a/src/codec_svt.c b/src/codec_svt.c
index 0a49955..e9a28c8 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -85,10 +85,26 @@
     int y_shift = 0;
     EbColorRange svt_range;
     if (alpha) {
+        // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The color_range field in the Sequence Header OBU shall be set to 1.
         svt_range = EB_CR_FULL_RANGE;
+
+        // AV1-AVIF specification, Section 4 "Auxiliary Image Items and Sequences":
+        //   The mono_chrome field in the Sequence Header OBU shall be set to 1.
+        // Some encoders do not support 4:0:0 and encode alpha as 4:2:0 so it is not always respected.
         y_shift = 1;
+
+        // CICP (CP/TC/MC) does not apply to the alpha auxiliary image.
+        // Use Unspecified (2) colour primaries, transfer characteristics, and matrix coefficients below.
     } else {
+        // AV1-ISOBMFF specification, Section 2.3.4:
+        //   The value of full_range_flag in the 'colr' box SHALL match the color_range
+        //   flag in the Sequence Header OBU.
         svt_range = (image->yuvRange == AVIF_RANGE_FULL) ? EB_CR_FULL_RANGE : EB_CR_STUDIO_RANGE;
+
+        // AV1-AVIF specification, Section 2.2.1. "AV1 Item Configuration Property":
+        //   The values of the fields in the AV1CodecConfigurationBox shall match those
+        //   of the Sequence Header OBU in the AV1 Image Item Data.
         switch (image->yuvFormat) {
             case AVIF_PIXEL_FORMAT_YUV444:
                 color_format = EB_YUV444;
@@ -125,6 +141,16 @@
         }
         svt_config->encoder_color_format = color_format;
         svt_config->encoder_bit_depth = (uint8_t)image->depth;
+
+        // Section 2.3.4 of AV1-ISOBMFF says 'colr' with 'nclx' should be present and shall match CICP
+        // values in the Sequence Header OBU, unless the latter has 2/2/2 (Unspecified).
+        // So set CICP values to 2/2/2 (Unspecified) in the Sequence Header OBU for simplicity.
+        // It may also save 3 bytes since the AV1 encoder may set color_description_present_flag to 0
+        // (see Section 5.5.2 "Color config syntax" of the AV1 specification).
+        svt_config->color_primaries = EB_CICP_CP_UNSPECIFIED;
+        svt_config->transfer_characteristics = EB_CICP_TC_UNSPECIFIED;
+        svt_config->matrix_coefficients = EB_CICP_MC_UNSPECIFIED;
+
         svt_config->color_range = svt_range;
 #if !SVT_AV1_CHECK_VERSION(0, 9, 0)
         svt_config->is_16bit_pipeline = image->depth > 8;
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 6cb09b7..6d4b40d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -86,6 +86,7 @@
     add_avif_gtest_with_data(avifanimationtest)
     add_avif_gtest(avifbasictest)
     add_avif_gtest(avifchangesettingtest)
+    add_avif_gtest(avifcicptest)
     add_avif_gtest(avifclaptest)
     add_avif_gtest(avifcllitest)
     add_avif_gtest(avifcodectest)
diff --git a/tests/data/goldens/kodim03_23_animation.avif.xml b/tests/data/goldens/kodim03_23_animation.avif.xml
index 5fbdfc1..d506ae9 100644
--- a/tests/data/goldens/kodim03_23_animation.avif.xml
+++ b/tests/data/goldens/kodim03_23_animation.avif.xml
@@ -124,7 +124,7 @@
  </OBUConfig>
  <OBUSamples>
   <Sample number="1" DTS="0" CTS="0" size="REDACTED" RAP="1" >
-   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="1" color_primaries="1" transfer_characteristics="13" matrix_coefficients="6" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
+   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="0" color_primaries="2" transfer_characteristics="2" matrix_coefficients="2" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
    <OBU size="REDACTED" type="frame" header_size="4" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" uncompressed_header_bytes="12" frame_type="key" refresh_frame_flags="255" show_frame="1" width="768" height="512" nb_tiles="2" >
      <Tile number="0" start="19" size="REDACTED"/>
      <Tile number="1" start="12994" size="REDACTED"/>
diff --git a/tests/data/goldens/kodim03_23_animation_keyframes.avif.xml b/tests/data/goldens/kodim03_23_animation_keyframes.avif.xml
index 882bc55..8823d73 100644
--- a/tests/data/goldens/kodim03_23_animation_keyframes.avif.xml
+++ b/tests/data/goldens/kodim03_23_animation_keyframes.avif.xml
@@ -122,7 +122,7 @@
  </OBUConfig>
  <OBUSamples>
   <Sample number="1" DTS="0" CTS="0" size="REDACTED" RAP="1" >
-   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="1" color_primaries="1" transfer_characteristics="13" matrix_coefficients="6" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
+   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="0" color_primaries="2" transfer_characteristics="2" matrix_coefficients="2" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
    <OBU size="REDACTED" type="frame" header_size="4" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" uncompressed_header_bytes="12" frame_type="key" refresh_frame_flags="255" show_frame="1" width="768" height="512" nb_tiles="2" >
      <Tile number="0" start="19" size="REDACTED"/>
      <Tile number="1" start="12994" size="REDACTED"/>
@@ -130,7 +130,7 @@
   </Sample>
 
   <Sample number="2" DTS="1" CTS="1" size="REDACTED" RAP="1" >
-   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="1" color_primaries="1" transfer_characteristics="13" matrix_coefficients="6" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
+   <OBU size="REDACTED" type="seq_header" header_size="2" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" sequence_width="768" sequence_height="512" still_picture="0" OperatingPointIdc="0" profile="0" level="4" bit_depth="8" monochrome="0" color_description_present_flag="0" color_primaries="2" transfer_characteristics="2" matrix_coefficients="2" color_range="1" chroma_subsampling_x="1" chroma_subsampling_y="1" chroma_sample_position="0" film_grain_params_present="0" />
    <OBU size="REDACTED" type="frame" header_size="4" has_size_field="1" has_ext="0" temporalID="0" spatialID="0" uncompressed_header_bytes="12" frame_type="key" refresh_frame_flags="255" show_frame="1" width="768" height="512" nb_tiles="2" >
      <Tile number="0" start="19" size="REDACTED"/>
      <Tile number="1" start="8808" size="REDACTED"/>
diff --git a/tests/gtest/avifcicptest.cc b/tests/gtest/avifcicptest.cc
new file mode 100644
index 0000000..1e4495a
--- /dev/null
+++ b/tests/gtest/avifcicptest.cc
@@ -0,0 +1,108 @@
+// Copyright 2025 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <tuple>
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace avif {
+namespace {
+
+class CicpTest
+    : public testing::TestWithParam<std::tuple<
+          avifCodecChoice, avifColorPrimaries, avifTransferCharacteristics,
+          avifMatrixCoefficients, avifPixelFormat, avifPlanesFlag, avifRange>> {
+};
+
+TEST_P(CicpTest, EncodeDecode) {
+  const avifCodecChoice codec_choice = std::get<0>(GetParam());
+  if (avifCodecName(codec_choice, AVIF_CODEC_FLAG_CAN_ENCODE) == nullptr) {
+    GTEST_SKIP() << "Codec unavailable, skip test.";
+  }
+  const avifColorPrimaries cp = std::get<1>(GetParam());
+  const avifTransferCharacteristics tc = std::get<2>(GetParam());
+  const avifMatrixCoefficients mc = std::get<3>(GetParam());
+  const avifPixelFormat subsampling = std::get<4>(GetParam());
+  const avifPlanesFlag planes = std::get<5>(GetParam());
+  const avifRange range = std::get<6>(GetParam());
+
+  ImagePtr image =
+      testutil::CreateImage(32, 32, /*depth=*/8, subsampling, planes, range);
+  ASSERT_NE(image, nullptr);
+  testutil::FillImageGradient(image.get());
+  image->colorPrimaries = cp;
+  image->transferCharacteristics = tc;
+  image->matrixCoefficients = mc;
+
+  EncoderPtr encoder(avifEncoderCreate());
+  ASSERT_NE(encoder, nullptr);
+  encoder->codecChoice = codec_choice;
+  encoder->speed = AVIF_SPEED_FASTEST;
+  testutil::AvifRwData encoded;
+  ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded),
+            AVIF_RESULT_OK);
+
+  if (testutil::Av1DecoderAvailable()) {
+    ImagePtr decoded(avifImageCreateEmpty());
+    ASSERT_NE(decoded, nullptr);
+    DecoderPtr decoder(avifDecoderCreate());
+    ASSERT_NE(decoder, nullptr);
+    ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
+                                    encoded.size),
+              AVIF_RESULT_OK);
+    EXPECT_EQ(decoded->colorPrimaries, cp);
+    EXPECT_EQ(decoded->transferCharacteristics, tc);
+    EXPECT_EQ(decoded->matrixCoefficients, mc);
+    EXPECT_EQ(decoded->yuvRange, range);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    Reserved0Identity, CicpTest,
+    // Identity MC require 4:4:4 and AVIF_CODEC_CHOICE_SVT only supports 4:2:0.
+    testing::Combine(
+        testing::Values(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_CHOICE_RAV1E),
+        testing::Values(static_cast<avifColorPrimaries>(
+            AVIF_COLOR_PRIMARIES_UNKNOWN)),  // Reserved CICP value.
+        testing::Values(static_cast<avifTransferCharacteristics>(
+            AVIF_TRANSFER_CHARACTERISTICS_UNKNOWN)),  // Reserved CICP value.
+        testing::Values(static_cast<avifMatrixCoefficients>(
+            AVIF_MATRIX_COEFFICIENTS_IDENTITY)),
+        testing::Values(AVIF_PIXEL_FORMAT_YUV444),
+        testing::Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL),
+        testing::Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL)));
+
+INSTANTIATE_TEST_SUITE_P(
+    Unspecified, CicpTest,
+    testing::Combine(testing::Values(AVIF_CODEC_CHOICE_AOM,
+                                     AVIF_CODEC_CHOICE_RAV1E,
+                                     AVIF_CODEC_CHOICE_SVT),
+                     testing::Values(static_cast<avifColorPrimaries>(
+                         AVIF_COLOR_PRIMARIES_UNSPECIFIED)),
+                     testing::Values(static_cast<avifTransferCharacteristics>(
+                         AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED)),
+                     testing::Values(static_cast<avifMatrixCoefficients>(
+                         AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED)),
+                     testing::Values(AVIF_PIXEL_FORMAT_YUV420),
+                     testing::Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL),
+                     testing::Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL)));
+
+INSTANTIATE_TEST_SUITE_P(
+    SrgbBt601, CicpTest,
+    testing::Combine(testing::Values(AVIF_CODEC_CHOICE_AOM,
+                                     AVIF_CODEC_CHOICE_RAV1E,
+                                     AVIF_CODEC_CHOICE_SVT),
+                     testing::Values(static_cast<avifColorPrimaries>(
+                         AVIF_COLOR_PRIMARIES_SRGB)),
+                     testing::Values(static_cast<avifTransferCharacteristics>(
+                         AVIF_TRANSFER_CHARACTERISTICS_SRGB)),
+                     testing::Values(static_cast<avifMatrixCoefficients>(
+                         AVIF_MATRIX_COEFFICIENTS_BT601)),
+                     testing::Values(AVIF_PIXEL_FORMAT_YUV420),
+                     testing::Values(AVIF_PLANES_YUV, AVIF_PLANES_ALL),
+                     testing::Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL)));
+
+}  // namespace
+}  // namespace avif