Move avifImageIsOpaque() from write.c to avif.h

Make it public so that it can be used in aviftest_helpers.cc and by
users.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59ba49c..2726765 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@
   should be updated to set quality (and qualityAlpha if applicable) and leave
   minQuantizer, maxQuantizer, minQuantizerAlpha, and maxQuantizerAlpha
   initialized to the default values.
+* Add the public API function avifImageIsOpaque() in avif.h.
 
 ### Changed
 * Exif and XMP metadata is exported to PNG and JPEG files by default,
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 775ce73..a91a121 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -412,7 +412,7 @@
         rgb.chromaUpsampling = chromaUpsampling;
         rgb.depth = rgbDepth;
         colorType = PNG_COLOR_TYPE_RGBA;
-        if (!avif->alphaPlane) {
+        if (avifImageIsOpaque(avif)) {
             colorType = PNG_COLOR_TYPE_RGB;
             rgb.format = AVIF_RGB_FORMAT_RGB;
         }
diff --git a/include/avif/avif.h b/include/avif/avif.h
index d778fa4..e4350d0 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -1175,6 +1175,7 @@
 
 // Helpers
 AVIF_API avifBool avifImageUsesU16(const avifImage * image);
+AVIF_API avifBool avifImageIsOpaque(const avifImage * image);
 // channel can be an avifChannelIndex.
 AVIF_API uint8_t * avifImagePlane(const avifImage * image, int channel);
 AVIF_API uint32_t avifImagePlaneRowBytes(const avifImage * image, int channel);
diff --git a/src/avif.c b/src/avif.c
index 75ea27c..595cf53 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -392,6 +392,34 @@
     return (image->depth > 8);
 }
 
+avifBool avifImageIsOpaque(const avifImage * image)
+{
+    if (!image->alphaPlane) {
+        return AVIF_TRUE;
+    }
+
+    const uint32_t opaqueValue = (1u << image->depth) - 1u;
+    const uint8_t * row = image->alphaPlane;
+    for (uint32_t y = 0; y < image->height; ++y) {
+        if (avifImageUsesU16(image)) {
+            const uint16_t * row16 = (const uint16_t *)row;
+            for (uint32_t x = 0; x < image->width; ++x) {
+                if (row16[x] != opaqueValue) {
+                    return AVIF_FALSE;
+                }
+            }
+        } else {
+            for (uint32_t x = 0; x < image->width; ++x) {
+                if (row[x] != opaqueValue) {
+                    return AVIF_FALSE;
+                }
+            }
+        }
+        row += image->alphaRowBytes;
+    }
+    return AVIF_TRUE;
+}
+
 uint8_t * avifImagePlane(const avifImage * image, int channel)
 {
     if ((channel == AVIF_CHAN_Y) || (channel == AVIF_CHAN_U) || (channel == AVIF_CHAN_V)) {
diff --git a/src/write.c b/src/write.c
index 7c420db..ff9117c 100644
--- a/src/write.c
+++ b/src/write.c
@@ -36,7 +36,6 @@
 static const char xmpContentType[] = AVIF_CONTENT_TYPE_XMP;
 static const size_t xmpContentTypeSize = sizeof(xmpContentType);
 
-static avifBool avifImageIsOpaque(const avifImage * image);
 static void writeConfigBox(avifRWStream * s, avifCodecConfigurationBox * cfg);
 
 // ---------------------------------------------------------------------------
@@ -1919,34 +1918,6 @@
     return avifEncoderFinish(encoder, output);
 }
 
-static avifBool avifImageIsOpaque(const avifImage * image)
-{
-    if (!image->alphaPlane) {
-        return AVIF_TRUE;
-    }
-
-    int maxChannel = (1 << image->depth) - 1;
-    if (avifImageUsesU16(image)) {
-        for (uint32_t j = 0; j < image->height; ++j) {
-            for (uint32_t i = 0; i < image->width; ++i) {
-                uint16_t * p = (uint16_t *)&image->alphaPlane[(i * 2) + (j * image->alphaRowBytes)];
-                if (*p != maxChannel) {
-                    return AVIF_FALSE;
-                }
-            }
-        }
-    } else {
-        for (uint32_t j = 0; j < image->height; ++j) {
-            for (uint32_t i = 0; i < image->width; ++i) {
-                if (image->alphaPlane[i + (j * image->alphaRowBytes)] != maxChannel) {
-                    return AVIF_FALSE;
-                }
-            }
-        }
-    }
-    return AVIF_TRUE;
-}
-
 static void writeConfigBox(avifRWStream * s, avifCodecConfigurationBox * cfg)
 {
     avifBoxMarker av1C = avifRWStreamWriteBox(s, "av1C", AVIF_BOX_SIZE_TBD);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 632ba7a..e340ff8 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -98,6 +98,11 @@
     target_include_directories(avifmetadatatest PRIVATE ${GTEST_INCLUDE_DIRS})
     add_test(NAME avifmetadatatest COMMAND avifmetadatatest ${CMAKE_CURRENT_SOURCE_DIR}/data/)
 
+    add_executable(avifopaquetest gtest/avifopaquetest.cc)
+    target_link_libraries(avifopaquetest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avifopaquetest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avifopaquetest COMMAND avifopaquetest)
+
     add_executable(avifrgbtoyuvtest gtest/avifrgbtoyuvtest.cc)
     target_link_libraries(avifrgbtoyuvtest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
     target_include_directories(avifrgbtoyuvtest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/avifopaquetest.cc b/tests/gtest/avifopaquetest.cc
new file mode 100644
index 0000000..5a48526
--- /dev/null
+++ b/tests/gtest/avifopaquetest.cc
@@ -0,0 +1,31 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace {
+
+TEST(OpaqueTest, AlphaAndNoAlpha) {
+  for (bool alpha_is_opaque : {false, true}) {
+    for (int depth : {8, 10, 12}) {
+      testutil::AvifImagePtr alpha = testutil::CreateImage(
+          1, 1, depth, AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL);
+      testutil::AvifImagePtr no_alpha = testutil::CreateImage(
+          1, 1, depth, AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_YUV);
+
+      const uint32_t max_value = (1u << depth) - 1;
+      const uint32_t yuva[] = {max_value, max_value, max_value,
+                               alpha_is_opaque ? max_value : (max_value - 1)};
+      testutil::FillImagePlain(alpha.get(), yuva);
+      testutil::FillImagePlain(no_alpha.get(), yuva);
+
+      EXPECT_EQ(testutil::AreImagesEqual(*alpha, *no_alpha), alpha_is_opaque);
+    }
+  }
+}
+
+}  // namespace
+}  // namespace libavif
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index daa2cc9..f48e970 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -166,6 +166,12 @@
     const uint8_t* row1 = avifImagePlane(&image1, c);
     const uint8_t* row2 = avifImagePlane(&image2, c);
     if (!row1 != !row2) {
+      // Maybe one image contains an opaque alpha channel while the other has no
+      // alpha channel, but they should still be considered equal.
+      if (c == AVIF_CHAN_A && avifImageIsOpaque(&image1) &&
+          avifImageIsOpaque(&image2)) {
+        continue;
+      }
       return false;
     }
     const uint32_t row_bytes1 = avifImagePlaneRowBytes(&image1, c);