Fix setting of cicp values in avifgainmaputil combine and convert. (#3110)

CICP must be set *before* calling ReadImage() so that the RGB->YUV
conversion uses the right matrix coefficient for PNG/JPEG files.
Also explicitly set the default matrix coefficients when not specified.

For Avif files, we overwrite the CICP after reading the file in ReadImage()
(keeps the previous behavior).

Fix --cicp-input flag not being used in tonemap command.

Fix memory leak when in 'combine' command when using with a file that already has a gain map.
Fix memory leak in 'tonemap' and 'swapbase' command with RGB pixels.

Fixes #3102 and #2869
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac49f96..658ee95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,8 @@
 * Update libxml2.cmd/LocalLibXml2.cmake: v2.15.2
 * Update libyuv.cmd/LocalLibyuv.cmake: 6067afde5 (1922)
 * Support long path names in Windows
+* Fix cicp management and memory leaks in avifgainmaputil
+  https://github.com/AOMediaCodec/libavif/issues/3102.
 
 ### Removed since 1.4.0
 
diff --git a/apps/avifgainmaputil/combine_command.cc b/apps/avifgainmaputil/combine_command.cc
index d32d036..654316d 100644
--- a/apps/avifgainmaputil/combine_command.cc
+++ b/apps/avifgainmaputil/combine_command.cc
@@ -3,6 +3,7 @@
 
 #include "combine_command.h"
 
+#include <cassert>
 #include <cmath>
 
 #include "avif/avif_cxx.h"
@@ -88,29 +89,21 @@
   if (base_image == nullptr || alternate_image == nullptr) {
     return AVIF_RESULT_OUT_OF_MEMORY;
   }
-  avifResult result =
-      ReadImage(base_image.get(), arg_base_filename_, pixel_format,
-                arg_image_read_.depth, arg_image_read_.ignore_profile);
-  if (result != AVIF_RESULT_OK) {
-    std::cout << "Failed to read base image: " << avifResultToString(result)
-              << "\n";
-    return result;
-  }
   if (arg_base_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
     base_image->colorPrimaries = arg_base_cicp_.value().color_primaries;
     base_image->transferCharacteristics =
         arg_base_cicp_.value().transfer_characteristics;
     base_image->matrixCoefficients = arg_base_cicp_.value().matrix_coefficients;
   }
-
-  result =
-      ReadImage(alternate_image.get(), arg_alternate_filename_, pixel_format,
-                arg_image_read_.depth, arg_image_read_.ignore_profile);
+  avifResult result = ReadImage(
+      base_image.get(), arg_base_filename_, pixel_format, arg_image_read_.depth,
+      arg_image_read_.ignore_profile, /*ignore_gain_map=*/true);
   if (result != AVIF_RESULT_OK) {
-    std::cout << "Failed to read alternate image: "
-              << avifResultToString(result) << "\n";
+    std::cout << "Failed to read base image: " << avifResultToString(result)
+              << "\n";
     return result;
   }
+
   if (arg_alternate_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
     alternate_image->colorPrimaries =
         arg_alternate_cicp_.value().color_primaries;
@@ -119,6 +112,14 @@
     alternate_image->matrixCoefficients =
         arg_alternate_cicp_.value().matrix_coefficients;
   }
+  result =
+      ReadImage(alternate_image.get(), arg_alternate_filename_, pixel_format,
+                arg_image_read_.depth, arg_image_read_.ignore_profile);
+  if (result != AVIF_RESULT_OK) {
+    std::cout << "Failed to read alternate image: "
+              << avifResultToString(result) << "\n";
+    return result;
+  }
 
   const uint32_t downscaling = std::max<int>(1, arg_downscaling_);
   const uint32_t rounding = downscaling / 2;
@@ -129,6 +130,10 @@
   std::cout << "Creating a gain map of size " << gain_map_width << " x "
             << gain_map_height << "\n";
 
+  // Because base_image is read with ignore_gain_map=true, there is no
+  // preexisting gain map. Otherwise, overwriting the pointer would cause a
+  // memory leak.
+  assert(base_image->gainMap == nullptr);
   base_image->gainMap = avifGainMapCreate();
   base_image->gainMap->image =
       avifImageCreate(gain_map_width, gain_map_height, arg_gain_map_depth_,
diff --git a/apps/avifgainmaputil/convert_command.cc b/apps/avifgainmaputil/convert_command.cc
index 58a7690..06bb949 100644
--- a/apps/avifgainmaputil/convert_command.cc
+++ b/apps/avifgainmaputil/convert_command.cc
@@ -52,32 +52,19 @@
     return AVIF_RESULT_OUT_OF_MEMORY;
   }
 
-  const avifAppFileFormat file_format = avifReadImage(
-      arg_input_filename_.value().c_str(),
-      AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */, pixel_format,
-      arg_image_read_.depth, AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC,
-      arg_image_read_.ignore_profile,
-      /*ignoreExif=*/false,
-      /*ignoreXMP=*/false,
-      /*ignoreGainMap=*/false, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(),
-      /*outDepth=*/nullptr,
-      /*sourceTiming=*/nullptr,
-      /*frameIter=*/nullptr);
-  if (file_format == AVIF_APP_FILE_FORMAT_UNKNOWN) {
-    std::cout << "Failed to decode image: " << arg_input_filename_;
-    return AVIF_RESULT_INVALID_ARGUMENT;
-  }
   if (arg_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
     image->colorPrimaries = arg_cicp_.value().color_primaries;
     image->transferCharacteristics = arg_cicp_.value().transfer_characteristics;
     image->matrixCoefficients = arg_cicp_.value().matrix_coefficients;
-  } else if (image->icc.size == 0 &&
-             image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED &&
-             image->transferCharacteristics ==
-                 AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
-    // If there is no ICC and no CICP, assume sRGB by default.
-    image->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB;
-    image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
+  }
+
+  avifResult result =
+      ReadImage(image.get(), arg_input_filename_.value(), pixel_format,
+                arg_image_read_.depth, arg_image_read_.ignore_profile,
+                /*ignore_gain_map*/ false);
+  if (result != AVIF_RESULT_OK) {
+    std::cout << "Failed to decode image: " << arg_input_filename_;
+    return result;
   }
 
   if (image->gainMap && image->gainMap->altICC.size == 0) {
@@ -110,8 +97,7 @@
     if (new_base == nullptr) {
       return AVIF_RESULT_OUT_OF_MEMORY;
     }
-    const avifResult result =
-        ChangeBase(*image, depth, image->yuvFormat, new_base.get());
+    result = ChangeBase(*image, depth, image->yuvFormat, new_base.get());
     if (result != AVIF_RESULT_OK) {
       return result;
     }
@@ -126,10 +112,9 @@
   encoder->qualityAlpha = arg_image_encode_.quality_alpha;
   encoder->qualityGainMap = arg_gain_map_quality_;
   encoder->speed = arg_image_encode_.speed;
-  const avifResult result =
-      WriteAvifGrid(image.get(), arg_image_encode_.grid.value().grid_cols,
-                    arg_image_encode_.grid.value().grid_rows, encoder.get(),
-                    arg_output_filename_);
+  result = WriteAvifGrid(image.get(), arg_image_encode_.grid.value().grid_cols,
+                         arg_image_encode_.grid.value().grid_rows,
+                         encoder.get(), arg_output_filename_);
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to encode image: " << avifResultToString(result)
               << " (" << encoder->diag.error << ")\n";
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 9a7e4e8..1686f55 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -155,7 +155,7 @@
 
 avifResult ReadImage(avifImage* image, const std::string& input_filename,
                      avifPixelFormat requested_format, uint32_t requested_depth,
-                     bool ignore_profile) {
+                     bool ignore_profile, bool ignore_gain_map) {
   avifAppFileFormat input_format = avifGuessFileFormat(input_filename.c_str());
   if (input_format == AVIF_APP_FILE_FORMAT_UNKNOWN) {
     std::cerr << "Cannot determine input format: " << input_filename;
@@ -165,10 +165,21 @@
     if (decoder == nullptr) {
       return AVIF_RESULT_OUT_OF_MEMORY;
     }
+    if (!ignore_gain_map) {
+      decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
+    }
     avifResult result = ReadAvif(decoder.get(), input_filename, ignore_profile);
     if (result != AVIF_RESULT_OK) {
       return result;
     }
+    if (ignore_gain_map && decoder->image->gainMap) {
+      avifGainMapDestroy(decoder->image->gainMap);
+      decoder->image->gainMap = nullptr;
+    }
+    const avifColorPrimaries in_primaries = image->colorPrimaries;
+    const avifTransferCharacteristics in_transfer =
+        image->transferCharacteristics;
+    const avifMatrixCoefficients in_matrix = image->matrixCoefficients;
     if (decoder->image->imageOwnsYUVPlanes &&
         (decoder->image->alphaPlane == nullptr ||
          decoder->image->imageOwnsAlphaPlane)) {
@@ -179,26 +190,37 @@
         return result;
       }
     }
+    if (in_primaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED ||
+        in_transfer != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED ||
+        in_matrix != AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
+      image->colorPrimaries = in_primaries;
+      image->transferCharacteristics = in_transfer;
+      image->matrixCoefficients = in_matrix;
+    }
   } else {
     const avifAppFileFormat file_format = avifReadImage(
         input_filename.c_str(), AVIF_APP_FILE_FORMAT_UNKNOWN /* guess format */,
         requested_format, static_cast<int>(requested_depth),
         AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, ignore_profile,
-        /*ignoreExif=*/false, /*ignoreXMP=*/false,
-        /*ignoreGainMap=*/true, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image,
+        /*ignoreExif=*/false, /*ignoreXMP=*/false, ignore_gain_map,
+        AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image,
         /*outDepth=*/nullptr,
         /*sourceTiming=*/nullptr, /*frameIter=*/nullptr);
     if (file_format == AVIF_APP_FILE_FORMAT_UNKNOWN) {
       std::cout << "Failed to decode image: " << input_filename;
       return AVIF_RESULT_INVALID_ARGUMENT;
     }
-    if (image->icc.size == 0) {
-      // Assume sRGB by default.
-      if (image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED &&
-          image->transferCharacteristics == AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
-        image->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB;
-        image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
-      }
+    // Assume sRGB by default.
+    if (image->icc.size == 0 &&
+        image->colorPrimaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED &&
+        image->transferCharacteristics == AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
+      image->colorPrimaries = AVIF_COLOR_PRIMARIES_SRGB;
+      image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_SRGB;
+    }
+    if (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
+      // Explicitly set the default matrix coefficient, see
+      // avifCalcYUVCoefficients().
+      image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601;
     }
   }
   return AVIF_RESULT_OK;
diff --git a/apps/avifgainmaputil/imageio.h b/apps/avifgainmaputil/imageio.h
index 441e5d7..c3d494b 100644
--- a/apps/avifgainmaputil/imageio.h
+++ b/apps/avifgainmaputil/imageio.h
@@ -19,7 +19,7 @@
 // Reads an image in any of the supported formats. Ignores any gain map.
 avifResult ReadImage(avifImage* image, const std::string& input_filename,
                      avifPixelFormat requested_format, uint32_t requested_depth,
-                     bool ignore_profile);
+                     bool ignore_profile, bool ignore_gain_map = true);
 
 // Reads an image in avif format given a pre-configured encoder.
 avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index e1d1e3b..e6e44ec 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -54,6 +54,7 @@
   }
 
   avifRGBImage swapped_rgb;
+  RGBImageCleanup rgb_cleanup(&swapped_rgb);
   avifRGBImageSetDefaults(&swapped_rgb, swapped);
 
   avifContentLightLevelInformationBox clli = image.gainMap->altCLLI;
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index c5e5520..1f2da42 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -69,6 +69,13 @@
   if (result != AVIF_RESULT_OK) {
     return result;
   }
+  if (arg_input_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
+    decoder->image->colorPrimaries = arg_input_cicp_.value().color_primaries;
+    decoder->image->transferCharacteristics =
+        arg_input_cicp_.value().transfer_characteristics;
+    decoder->image->matrixCoefficients =
+        arg_input_cicp_.value().matrix_coefficients;
+  }
 
   avifImage* image = decoder->image;
   if (image->gainMap == nullptr || image->gainMap->image == nullptr) {
@@ -115,10 +122,9 @@
             image->gainMap->altMatrixCoefficients};
   }
   if (cicp.color_primaries == AVIF_COLOR_PRIMARIES_UNSPECIFIED) {
-    // TODO(maryla): for now avifImageApplyGainMap always uses the primaries of
-    // the base image, but it should take into account the metadata's
-    // useBaseColorSpace property.
-    cicp.color_primaries = image->colorPrimaries;
+    cicp.color_primaries = image->gainMap->useBaseColorSpace
+                               ? image->colorPrimaries
+                               : image->gainMap->altColorPrimaries;
   }
   if (cicp.transfer_characteristics ==
       AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED) {
@@ -181,7 +187,13 @@
   if (tone_mapped == nullptr) {
     return AVIF_RESULT_OUT_OF_MEMORY;
   }
+  tone_mapped->colorPrimaries = cicp.color_primaries;
+  tone_mapped->transferCharacteristics = cicp.transfer_characteristics;
+  tone_mapped->matrixCoefficients = cicp.matrix_coefficients;
+  tone_mapped->clli = clli_box;
+
   avifRGBImage tone_mapped_rgb;
+  RGBImageCleanup rgb_cleanup(&tone_mapped_rgb);
   avifRGBImageSetDefaults(&tone_mapped_rgb, tone_mapped.get());
   avifDiagnostics diag;
   result = avifImageApplyGainMap(
@@ -200,11 +212,6 @@
     return result;
   }
 
-  tone_mapped->clli = clli_box;
-  tone_mapped->transferCharacteristics = cicp.transfer_characteristics;
-  tone_mapped->colorPrimaries = cicp.color_primaries;
-  tone_mapped->matrixCoefficients = cicp.matrix_coefficients;
-
   return WriteImage(tone_mapped.get(), arg_image_encode_.grid.value().grid_cols,
                     arg_image_encode_.grid.value().grid_rows,
                     arg_output_filename_, arg_image_encode_.quality,
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index dee2119..bb1199b 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -267,10 +267,9 @@
     add_cmd_test(test_cmd_stdin ${CMAKE_CURRENT_SOURCE_DIR}/data)
     add_cmd_test(test_cmd_targetsize ${CMAKE_CURRENT_SOURCE_DIR}/data)
     add_cmd_test(test_cmd_transform ${CMAKE_CURRENT_SOURCE_DIR}/data)
+    add_cmd_test(test_cmd_avifgainmaputil ${CMAKE_CURRENT_SOURCE_DIR}/data)
 
-    if(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
-        add_cmd_test(test_cmd_avifgainmaputil ${CMAKE_CURRENT_SOURCE_DIR}/data)
-    else()
+    if(NOT AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION)
         set_tests_properties(test_cmd_stdin PROPERTIES DISABLED True)
     endif()
 
@@ -361,6 +360,7 @@
                 avifrangetest
                 aviftunetest
                 avify4mtest
+                test_cmd_avifgainmaputil
                 PROPERTIES DISABLED True
             )
 
diff --git a/tests/data/README.md b/tests/data/README.md
index 25fc03c..1ad798b 100644
--- a/tests/data/README.md
+++ b/tests/data/README.md
@@ -823,6 +823,18 @@
 
 SDR image with a gain map to allow tone mapping to HDR. The gain map's width and height are halved compared to the base image.
 
+### File [seine_sdr_gainmap_srgb_icc.avif](seine_sdr_gainmap_srgb_icc.avif)
+
+![](seine_sdr_gainmap_srgb_icc.avif)
+
+License: [same as libavif](https://github.com/AOMediaCodec/libavif/blob/main/LICENSE)
+
+Source : created by running the following command with avifenc compiled with libxml enabled:
+
+```bash
+avifenc seine_sdr_gainmap_srgb.jpg seine_sdr_gainmap_srgb_icc.avif --qcolor 90 --qgain-map 90
+```
+
 ## Files colors*_hdr_*.avif and colors*_sdr_srgb.avif
 
 ![](colors_wcg_hdr_rec2020.avif)
diff --git a/tests/data/seine_sdr_gainmap_srgb_icc.avif b/tests/data/seine_sdr_gainmap_srgb_icc.avif
new file mode 100644
index 0000000..a020ba6
--- /dev/null
+++ b/tests/data/seine_sdr_gainmap_srgb_icc.avif
Binary files differ
diff --git a/tests/test_cmd_avifgainmaputil.sh b/tests/test_cmd_avifgainmaputil.sh
index d339aac..9a30acb 100755
--- a/tests/test_cmd_avifgainmaputil.sh
+++ b/tests/test_cmd_avifgainmaputil.sh
@@ -15,9 +15,9 @@
 # Input file paths.
 INPUT_AVIF_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.avif"
 INPUT_AVIF_GAINMAP_HDR="${TESTDATA_DIR}/seine_hdr_gainmap_srgb.avif"
-INPUT_AVIF_GAINMAP_HDR2020="${TESTDATA_DIR}/seine_hdr_rec2020.avif"
-INPUT_JPEG_AVIF_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.jpg"
-AVIF_GAINMAP_SDR_WITH_ICC="seine_sdr_gainmap_srgb_icc.jpg" # generated below
+INPUT_AVIF_HDR2020="${TESTDATA_DIR}/seine_hdr_rec2020.avif"
+INPUT_JPEG_GAINMAP_SDR="${TESTDATA_DIR}/seine_sdr_gainmap_srgb.jpg"
+INPUT_AVIF_GAINMAP_SDR_WITH_ICC="${TESTDATA_DIR}/seine_sdr_gainmap_srgb_icc.avif"
 # Output file names.
 AVIF_OUTPUT="avif_test_cmd_avifgainmaputil_output.avif"
 JPEG_OUTPUT="avif_test_cmd_avifgainmaputil_output.jpg"
@@ -25,16 +25,11 @@
 
 # Cleanup
 cleanup() {
-  pushd ${TMP_DIR}
-    rm -- "${AVIF_OUTPUT}" "${JPEG_OUTPUT}" "${PNG_OUTPUT}" "${AVIF_GAINMAP_SDR_WITH_ICC}"
-  popd
+  rm -f -r "${TMP_DIR}"
 }
 trap cleanup EXIT
 
 pushd ${TMP_DIR}
-  # generate another input file used in tests below
-  "${AVIFENC}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${AVIF_GAINMAP_SDR_WITH_ICC}" --qcolor 90 --qgain-map 90
-
   "${AVIFGAINMAPUTIL}" help
 
   "${AVIFGAINMAPUTIL}" printmetadata "${INPUT_AVIF_GAINMAP_SDR}"
@@ -45,33 +40,63 @@
 
   "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
       -q 50 --downscaling 2 --yuv-gain-map 400
-  "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
+  "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
       -q 50 --qgain-map 90 && exit 1 # should fail because icc profiles are not supported
-  "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
+  "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR}" "${AVIF_OUTPUT}" \
       -q 50 --qgain-map 90 --ignore-profile
-  "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_GAINMAP_HDR2020}" "${AVIF_OUTPUT}" \
+  "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_SDR}" "${INPUT_AVIF_HDR2020}" "${AVIF_OUTPUT}" \
       -q 50 --downscaling 2 --yuv-gain-map 400 --grid 2x2
 
   "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_GAINMAP_HDR}" "${INPUT_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" \
       -q 90 --qgain-map 90
   "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${PNG_OUTPUT}" --headroom 0
   "${AVIFGAINMAPUTIL}" tonemap "${INPUT_AVIF_GAINMAP_SDR}" "${PNG_OUTPUT}" --headroom 0 --clli 400,500
-  "${ARE_IMAGES_EQUAL}" "${PNG_OUTPUT}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" 0 40
+  "${ARE_IMAGES_EQUAL}" "${PNG_OUTPUT}" "${INPUT_JPEG_GAINMAP_SDR}" 0 40
+
+  # Test combine with overriden cicp values. Matrix coefficient 0 (identity) makes it obvious if there is an issue.
+  "${AVIFGAINMAPUTIL}" combine "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_AVIF_HDR2020}" "${AVIF_OUTPUT}" \
+      -q 100 --qgain-map 100 --cicp-base 1/13/0 --ignore-profile
+  # Tone map to SDR and compare with original SDR.
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${PNG_OUTPUT}" --headroom 0
+  "${ARE_IMAGES_EQUAL}" "${PNG_OUTPUT}" "${INPUT_JPEG_GAINMAP_SDR}" 0 99
+  # Tone map to HDR and compare with original HDR.
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.tonemapped.png" --headroom 2
+  # are_images_equal doesn't support AVIF so we convert to PNG.
+  "${AVIFDEC}" "${INPUT_AVIF_HDR2020}" "input_avif_hdr2020.png"
+  "${ARE_IMAGES_EQUAL}" "${AVIF_OUTPUT}.tonemapped.png" "input_avif_hdr2020.png" 0 60 # A bit of loss from going through gainmap
+
+  # Same as above but HDR base.
+  "${AVIFGAINMAPUTIL}" combine "${INPUT_AVIF_HDR2020}"  "${INPUT_JPEG_GAINMAP_SDR}" "${AVIF_OUTPUT}" \
+      -q 100 --qgain-map 100 --cicp-alternate 1/13/0 --ignore-profile
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${PNG_OUTPUT}" --headroom 0
+  "${ARE_IMAGES_EQUAL}" "${PNG_OUTPUT}" "${INPUT_JPEG_GAINMAP_SDR}" 0 50 # A bit of loss from going through gainmap
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.tonemapped.png" --headroom 2
+  # are_images_equal doesn't support AVIF so we convert to PNG.
+  "${AVIFDEC}" "${INPUT_AVIF_HDR2020}" "input_avif_hdr2020.png"
+  "${ARE_IMAGES_EQUAL}" "${AVIF_OUTPUT}.tonemapped.png" "input_avif_hdr2020.png" 0 90
 
   "${AVIFGAINMAPUTIL}" swapbase "${INPUT_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" --qcolor 90 --qgain-map 90
   # should fail because icc profiles are not supported
-  "${AVIFGAINMAPUTIL}" swapbase "${AVIF_GAINMAP_SDR_WITH_ICC}" "${AVIF_OUTPUT}" --qcolor 90 --qgain-map 90 && exit 1
-  "${AVIFGAINMAPUTIL}" swapbase "${AVIF_GAINMAP_SDR_WITH_ICC}" "${AVIF_OUTPUT}" --qcolor 90 --qgain-map 90 --ignore-profile
+  "${AVIFGAINMAPUTIL}" swapbase "${INPUT_AVIF_GAINMAP_SDR_WITH_ICC}" "${AVIF_OUTPUT}" --qcolor 90 --qgain-map 90 && exit 1
+  "${AVIFGAINMAPUTIL}" swapbase "${INPUT_AVIF_GAINMAP_SDR_WITH_ICC}" "${AVIF_OUTPUT}" --qcolor 90 --qgain-map 90 --ignore-profile
 
-  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}"
+   # Also test the are_images_equal binary itself with some gain maps
+  "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_JPEG_GAINMAP_SDR}" 0 40 0
+  "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_JPEG_GAINMAP_SDR}" 0 40 1
+
+  # Check if avifgainmaputil was built with libxml2.
+  # If it was not, the 'convert' command will fail with an error message
+  # containing "libxml2".
+  if "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_GAINMAP_SDR}" "${AVIF_OUTPUT}" 2>&1 | grep -q "libxml2"; then
+    echo "avifgainmaputil was built without libxml2, skipping convert tests."
+    popd
+    exit 0
+  fi
+  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_GAINMAP_SDR}" "${AVIF_OUTPUT}"
    # should fail because icc profiles are not supported
-  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" --swap-base && exit 1
-  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${AVIF_OUTPUT}" --swap-base --ignore-profile \
+  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_GAINMAP_SDR}" "${AVIF_OUTPUT}" --swap-base && exit 1
+  "${AVIFGAINMAPUTIL}" convert "${INPUT_JPEG_GAINMAP_SDR}" "${AVIF_OUTPUT}" --swap-base --ignore-profile \
       --cicp 2/3/4
-
-   # also test the are_images_equal binary itself with some gain maps
-  "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" 0 40 0
-  "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" "${INPUT_JPEG_AVIF_GAINMAP_SDR}" 0 40 1
 popd
 
 exit 0