avifgainmaputil: add --ignore-alpha flag to discard alpha channel (#3273)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68f62f5..0ba9e94 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 ### Added since 1.4.2
 
 * avifenc: add --ignore-alpha flag to discard alpha channel on encode
+* avifgainmaputil: add --ignore-alpha flag to discard alpha channel
 
 ### Changed since 1.4.2
 
diff --git a/apps/avifgainmaputil/combine_command.cc b/apps/avifgainmaputil/combine_command.cc
index ce59cb6..97925f9 100644
--- a/apps/avifgainmaputil/combine_command.cc
+++ b/apps/avifgainmaputil/combine_command.cc
@@ -96,10 +96,10 @@
         arg_base_cicp_.value().transfer_characteristics;
     base_image->matrixCoefficients = arg_base_cicp_.value().matrix_coefficients;
   }
-  avifResult result =
-      ReadImage(base_image.get(), arg_base_filename_, pixel_format,
-                arg_image_read_.depth, arg_image_read_.ignore_profile,
-                /*ignore_gain_map=*/true, arg_jobs_.jobs.value());
+  avifResult result = ReadImage(
+      base_image.get(), arg_base_filename_, pixel_format, arg_image_read_.depth,
+      arg_image_read_.ignore_profile, arg_image_read_.ignore_alpha,
+      /*ignore_gain_map=*/true, arg_jobs_.jobs.value());
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to read base image: " << avifResultToString(result)
               << "\n";
@@ -117,7 +117,8 @@
   result =
       ReadImage(alternate_image.get(), arg_alternate_filename_, pixel_format,
                 arg_image_read_.depth, arg_image_read_.ignore_profile,
-                /*ignore_gain_map=*/true, arg_jobs_.jobs.value());
+                arg_image_read_.ignore_alpha, /*ignore_gain_map=*/true,
+                arg_jobs_.jobs.value());
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to read alternate image: "
               << avifResultToString(result) << "\n";
diff --git a/apps/avifgainmaputil/convert_command.cc b/apps/avifgainmaputil/convert_command.cc
index 1cbe74e..54e4744 100644
--- a/apps/avifgainmaputil/convert_command.cc
+++ b/apps/avifgainmaputil/convert_command.cc
@@ -62,7 +62,8 @@
   avifResult result =
       ReadImage(image.get(), arg_input_filename_.value(), pixel_format,
                 arg_image_read_.depth, arg_image_read_.ignore_profile,
-                /*ignore_gain_map=*/false, arg_jobs_.jobs.value());
+                arg_image_read_.ignore_alpha, /*ignore_gain_map=*/false,
+                arg_jobs_.jobs.value());
   if (result != AVIF_RESULT_OK) {
     std::cout << "Failed to decode image: " << arg_input_filename_;
     return result;
diff --git a/apps/avifgainmaputil/extractgainmap_command.cc b/apps/avifgainmaputil/extractgainmap_command.cc
index 6ec9e5e..5779ab8 100644
--- a/apps/avifgainmaputil/extractgainmap_command.cc
+++ b/apps/avifgainmaputil/extractgainmap_command.cc
@@ -26,7 +26,8 @@
   decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_GAIN_MAP;
 
   avifResult result =
-      ReadAvif(decoder.get(), arg_input_filename_, /*ignore_profile=*/true);
+      ReadAvif(decoder.get(), arg_input_filename_, /*ignore_profile=*/true,
+               /*ignore_alpha=*/false);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 05b55f6..7786301 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -190,7 +190,8 @@
 
 avifResult ReadImage(avifImage* image, const std::string& input_filename,
                      avifPixelFormat requested_format, uint32_t requested_depth,
-                     bool ignore_profile, bool ignore_gain_map, int jobs) {
+                     bool ignore_profile, bool ignore_alpha,
+                     bool ignore_gain_map, int jobs) {
   avifAppFileFormat input_format = avifGuessFileFormat(input_filename.c_str());
   if (input_format == AVIF_APP_FILE_FORMAT_UNKNOWN) {
     std::cerr << "Cannot determine input format: " << input_filename;
@@ -204,7 +205,8 @@
       decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
     }
     decoder->maxThreads = jobs;
-    avifResult result = ReadAvif(decoder.get(), input_filename, ignore_profile);
+    avifResult result =
+        ReadAvif(decoder.get(), input_filename, ignore_profile, ignore_alpha);
     if (result != AVIF_RESULT_OK) {
       return result;
     }
@@ -238,7 +240,7 @@
         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, /*ignoreAlpha=*/false,
+        /*ignoreExif=*/false, /*ignoreXMP=*/false, ignore_alpha,
         ignore_gain_map, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image,
         /*outDepth=*/nullptr,
         /*sourceTiming=*/nullptr, /*frameIter=*/nullptr);
@@ -263,7 +265,7 @@
 }
 
 avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename,
-                    bool ignore_profile) {
+                    bool ignore_profile, bool ignore_alpha) {
   avifResult result = avifDecoderSetIOFile(decoder, input_filename.c_str());
   if (result != AVIF_RESULT_OK) {
     std::cerr << "Cannot open file for read: " << input_filename << "\n";
@@ -287,6 +289,9 @@
       avifRWDataFree(&decoder->image->gainMap->altICC);
     }
   }
+  if (ignore_alpha && decoder->image->alphaPlane) {
+    avifImageFreePlanes(decoder->image, AVIF_PLANES_A);
+  }
 
   return AVIF_RESULT_OK;
 }
diff --git a/apps/avifgainmaputil/imageio.h b/apps/avifgainmaputil/imageio.h
index bc69c63..7b567b8 100644
--- a/apps/avifgainmaputil/imageio.h
+++ b/apps/avifgainmaputil/imageio.h
@@ -19,7 +19,8 @@
 // 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_gain_map, int jobs);
+                     bool ignore_profile, bool ignore_alpha,
+                     bool ignore_gain_map, int jobs);
 
 // Reads an image in avif format given a pre-configured encoder.
 avifResult WriteAvif(const avifImage* image, avifEncoder* encoder,
@@ -33,7 +34,7 @@
 // Reads an image in avif format given a pre-configured decoder.
 // The image can be accessed at decoder->image.
 avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename,
-                    bool ignore_profile);
+                    bool ignore_profile, bool ignore_alpha);
 
 }  // namespace avif
 
diff --git a/apps/avifgainmaputil/program_command.h b/apps/avifgainmaputil/program_command.h
index b579253..5c18b0b 100644
--- a/apps/avifgainmaputil/program_command.h
+++ b/apps/avifgainmaputil/program_command.h
@@ -133,6 +133,7 @@
   argparse::ArgValue<int> depth;
   argparse::ArgValue<int> pixel_format;
   argparse::ArgValue<bool> ignore_profile;
+  argparse::ArgValue<bool> ignore_alpha;
 
   void Init(argparse::ArgumentParser& argparse) {
     argparse
@@ -147,6 +148,12 @@
             "(no-op if absent)")
         .action(argparse::Action::STORE_TRUE)
         .default_value("false");
+    argparse.add_argument(ignore_alpha, "--ignore-alpha")
+        .help(
+            "If the input file contains an alpha channel, ignore it "
+            "(no-op if absent)")
+        .action(argparse::Action::STORE_TRUE)
+        .default_value("false");
   }
 };
 
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index cff3f77..e774fd8 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -56,6 +56,9 @@
   avifRGBImage swapped_rgb;
   RGBImageCleanup rgb_cleanup(&swapped_rgb);
   avifRGBImageSetDefaults(&swapped_rgb, swapped);
+  if (image.alphaPlane == nullptr) {
+    swapped_rgb.format = AVIF_RGB_FORMAT_RGB;
+  }
 
   avifContentLightLevelInformationBox clli = image.gainMap->altCLLI;
   const bool compute_clli =
@@ -161,8 +164,9 @@
   }
   decoder->maxThreads = arg_jobs_.jobs.value();
   decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
-  avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
-                               arg_image_read_.ignore_profile);
+  avifResult result =
+      ReadAvif(decoder.get(), arg_input_filename_,
+               arg_image_read_.ignore_profile, arg_image_read_.ignore_alpha);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index fa501ea..270096a 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -66,8 +66,9 @@
   }
   decoder->maxThreads = arg_jobs_.jobs.value();
   decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
-  avifResult result = ReadAvif(decoder.get(), arg_input_filename_,
-                               arg_image_read_.ignore_profile);
+  avifResult result =
+      ReadAvif(decoder.get(), arg_input_filename_,
+               arg_image_read_.ignore_profile, arg_image_read_.ignore_alpha);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
@@ -197,6 +198,9 @@
   avifRGBImage tone_mapped_rgb;
   RGBImageCleanup rgb_cleanup(&tone_mapped_rgb);
   avifRGBImageSetDefaults(&tone_mapped_rgb, tone_mapped.get());
+  if (image->alphaPlane == nullptr) {
+    tone_mapped_rgb.format = AVIF_RGB_FORMAT_RGB;
+  }
   avifDiagnostics diag;
   result = avifImageApplyGainMap(
       decoder->image, image->gainMap, arg_headroom_, cicp.color_primaries,
diff --git a/tests/test_cmd_avifgainmaputil.sh b/tests/test_cmd_avifgainmaputil.sh
index e108d8a..b25061d 100755
--- a/tests/test_cmd_avifgainmaputil.sh
+++ b/tests/test_cmd_avifgainmaputil.sh
@@ -118,6 +118,38 @@
   LC_ALL=C sed 's/<rdf:li>1/<rdf:li>2/' "${INPUT_JPEG_GAINMAP_SDR}" > "${INPUT_JPEG_GAINMAP_SDR_MODIFIED}"
   "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_JPEG_GAINMAP_SDR_MODIFIED}" 0 0 0 && exit 1
   "${ARE_IMAGES_EQUAL}" "${INPUT_JPEG_GAINMAP_SDR}" "${INPUT_JPEG_GAINMAP_SDR_MODIFIED}" 0 30 0 && exit 1
+
+  # Test --ignore-alpha.
+  echo "Testing --ignore-alpha"
+  # Combine PNG with alpha. Output should have alpha.
+  "${AVIFGAINMAPUTIL}" combine "${TESTDATA_DIR}/circle-trns-after-plte.png" "${TESTDATA_DIR}/circle-trns-after-plte.png" "${AVIF_OUTPUT}" \
+      -q 50 --ignore-profile > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Not premultiplied" "${OUT_MSG}"
+  # Combine PNG with alpha and --ignore-alpha. Output should NOT have alpha.
+  "${AVIFGAINMAPUTIL}" combine "${TESTDATA_DIR}/circle-trns-after-plte.png" "${TESTDATA_DIR}/circle-trns-after-plte.png" "${AVIF_OUTPUT}" \
+      -q 50 --ignore-profile --ignore-alpha > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Absent" "${OUT_MSG}"
+  # Re-create AVIF with alpha.
+  "${AVIFGAINMAPUTIL}" combine "${TESTDATA_DIR}/circle-trns-after-plte.png" "${TESTDATA_DIR}/circle-trns-after-plte.png" "${AVIF_OUTPUT}" \
+      -q 50 --ignore-profile > /dev/null
+  # Tonemap AVIF with alpha. Output should have alpha.
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.tonemapped.avif" --headroom 0 > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Not premultiplied" "${OUT_MSG}"
+  # Tonemap AVIF with alpha and --ignore-alpha. Output should NOT have alpha.
+  "${AVIFGAINMAPUTIL}" tonemap "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.tonemapped.avif" --headroom 0 --ignore-alpha > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Absent" "${OUT_MSG}"
+  # Swapbase AVIF with alpha. Output should have alpha.
+  "${AVIFGAINMAPUTIL}" swapbase "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.swapped.avif" --qcolor 90 --qgain-map 90 > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Not premultiplied" "${OUT_MSG}"
+  # Swapbase AVIF with alpha and --ignore-alpha. Output should NOT have alpha.
+  "${AVIFGAINMAPUTIL}" swapbase "${AVIF_OUTPUT}" "${AVIF_OUTPUT}.swapped.avif" --qcolor 90 --qgain-map 90 --ignore-alpha > "${OUT_MSG}"
+  cat "${OUT_MSG}"
+  grep " Alpha          : Absent" "${OUT_MSG}"
 popd
 
 exit 0