Skip sato tiles depending on imageContentToDecode (#2981)

Do not generate tiles for Sample Transform input image items unless
imageContentToDecode&AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA!=0.
Explicitly test any expected avifItemCategory in avifDecoderReset()
and return an error for unlisted avifItemCategory values.
Also guard avifDecoderApplySampleTransform() with imageContentToDecode
in case there are other tiles, such as gain map items.
Add test for the non-gain-map case because libavif does not support
encoding gain maps in the same file as the 'sato' items.
diff --git a/src/read.c b/src/read.c
index 947f273..3a8581a 100644
--- a/src/read.c
+++ b/src/read.c
@@ -6357,6 +6357,7 @@
             if (!mainItems[c]) {
                 continue;
             }
+            AVIF_ASSERT_OR_RETURN(c != AVIF_ITEM_SAMPLE_TRANSFORM); // See sampleTransformItem.
 
             if (avifIsAlpha((avifItemCategory)c) && !mainItems[c]->width && !mainItems[c]->height) {
                 // NON-STANDARD: Alpha subimage does not have an ispe property; adopt width/height from color item
@@ -6367,8 +6368,17 @@
 
             AVIF_CHECKRES(avifDecoderAdoptGridTileCodecTypeIfNeeded(decoder, mainItems[c], &data->tileInfos[c]));
 
-            if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) && (c == AVIF_ITEM_COLOR || c == AVIF_ITEM_ALPHA)) {
-                continue;
+            if (c == AVIF_ITEM_COLOR || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR ||
+                c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_COLOR || c == AVIF_ITEM_ALPHA ||
+                c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_ALPHA) {
+                if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA)) {
+                    continue;
+                }
+            } else {
+                AVIF_ASSERT_OR_RETURN(c == AVIF_ITEM_GAIN_MAP);
+                if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_GAIN_MAP)) {
+                    continue;
+                }
             }
 
             AVIF_CHECKRES(avifDecoderGenerateImageTiles(decoder, &data->tileInfos[c], mainItems[c], (avifItemCategory)c));
@@ -6929,7 +6939,7 @@
         AVIF_ASSERT_OR_RETURN(prepareTileResult[c] == AVIF_RESULT_OK);
     }
 
-    if (decoder->data->meta->sampleTransformExpression.count > 0) {
+    if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) && decoder->data->meta->sampleTransformExpression.count > 0) {
         // TODO(yguyon): Add a field in avifDecoder and only perform sample transformations upon request.
         AVIF_CHECKRES(avifDecoderApplySampleTransform(decoder, decoder->image));
     }
diff --git a/src/write.c b/src/write.c
index 739f99b..f914616 100644
--- a/src/write.c
+++ b/src/write.c
@@ -1955,8 +1955,9 @@
             encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_4B ||
             encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_12B_8B_OVERLAP_4B) {
             // For now, only 16-bit depth is supported.
-            AVIF_ASSERT_OR_RETURN(firstCell->depth == 16);
-            AVIF_CHECKERR(!firstCell->gainMap, AVIF_RESULT_NOT_IMPLEMENTED); // TODO(yguyon): Implement 16-bit HDR
+            AVIF_CHECKERR(firstCell->depth == 16, AVIF_RESULT_NOT_IMPLEMENTED);
+            // TODO: b/480081865 - Support gain maps in same file as Sample Transforms
+            AVIF_CHECKERR(!firstCell->gainMap, AVIF_RESULT_NOT_IMPLEMENTED);
             AVIF_CHECKRES(avifEncoderCreateBitDepthExtensionItems(encoder, gridCols, gridRows, gridWidth, gridHeight, colorItemID));
         } else {
             AVIF_CHECKERR(encoder->sampleTransformRecipe == AVIF_SAMPLE_TRANSFORM_NONE, AVIF_RESULT_NOT_IMPLEMENTED);
diff --git a/tests/gtest/avif16bittest.cc b/tests/gtest/avif16bittest.cc
index 20d5650..3266a8f 100644
--- a/tests/gtest/avif16bittest.cc
+++ b/tests/gtest/avif16bittest.cc
@@ -211,6 +211,132 @@
         testing::Values(AVIF_QUALITY_LOSSLESS),
         /*add_xmp=*/testing::Values(true)));
 
+//------------------------------------------------------------------------------
+
+void CreateGainMap(avifImage* image) {
+  avifGainMap* gainmap = avifGainMapCreate();
+  ASSERT_NE(gainmap, nullptr);
+  gainmap->image = avifImageCreateEmpty();
+  ASSERT_NE(gainmap->image, nullptr);
+  const avifCropRect rect{0, 0, image->width, image->height};
+  ASSERT_EQ(avifImageSetViewRect(gainmap->image, image, &rect), AVIF_RESULT_OK);
+  gainmap->image->depth = 8;  // 'sato' gain maps are not supported.
+  image->gainMap = gainmap;
+}
+
+class GainmapSampleTransformTest
+    : public testing::TestWithParam<
+          std::tuple<avifSampleTransformRecipe, /*create_alpha=*/bool,
+                     /*create_gainmap=*/bool, /*use_grid=*/bool,
+                     avifImageContentTypeFlags>> {};
+
+TEST_P(GainmapSampleTransformTest, ImageContentToDecode) {
+  const avifSampleTransformRecipe recipe = std::get<0>(GetParam());
+  const bool create_alpha = std::get<1>(GetParam());
+  const bool create_gainmap = std::get<2>(GetParam());
+  const bool use_grid = std::get<3>(GetParam());
+  const avifImageContentTypeFlags content_to_decode = std::get<4>(GetParam());
+
+  const ImagePtr image =
+      testutil::ReadImage(data_path, "weld_16bit.png", AVIF_PIXEL_FORMAT_YUV444,
+                          /*requested_depth=*/16);
+  // Speed test up.
+  image->width = 128;
+  image->height = 128;
+  ASSERT_NE(image, nullptr);
+  if (create_alpha && !image->alphaPlane) {
+    // Simulate alpha plane with a view on luma.
+    image->alphaPlane = image->yuvPlanes[AVIF_CHAN_Y];
+    image->alphaRowBytes = image->yuvRowBytes[AVIF_CHAN_Y];
+    image->imageOwnsAlphaPlane = false;
+  }
+  if (create_gainmap && !image->gainMap) {
+    // Simulate a gainmap with a view on the base image.
+    CreateGainMap(image.get());
+  }
+
+  EncoderPtr encoder(avifEncoderCreate());
+  ASSERT_NE(encoder, nullptr);
+  encoder->speed = AVIF_SPEED_FASTEST;
+  encoder->sampleTransformRecipe = recipe;
+  testutil::AvifRwData encoded;
+  const uint64_t kDurationInTimescales = 1;
+  const avifAddImageFlags kAddImageFlags = AVIF_ADD_IMAGE_FLAG_SINGLE;
+  if (use_grid) {
+    const uint32_t kGridCols = 2, kGridRows = 1;
+    const ImagePtr cell0{avifImageCreateEmpty()};
+    const ImagePtr cell1{avifImageCreateEmpty()};
+    ASSERT_NE(cell0, nullptr);
+    ASSERT_NE(cell1, nullptr);
+    const avifCropRect rect0{0, 0, image->width / kGridCols, image->height};
+    const avifCropRect rect1{rect0.width, 0, image->width - rect0.width,
+                             image->height};
+    ASSERT_EQ(avifImageSetViewRect(cell0.get(), image.get(), &rect0),
+              AVIF_RESULT_OK);
+    ASSERT_EQ(avifImageSetViewRect(cell1.get(), image.get(), &rect1),
+              AVIF_RESULT_OK);
+    ASSERT_EQ(
+        avifImageSetMetadataXMP(cell0.get(), image->xmp.data, image->xmp.size),
+        AVIF_RESULT_OK);
+    if (create_gainmap) {
+      CreateGainMap(cell0.get());
+      CreateGainMap(cell1.get());
+    }
+    const avifImage* cells[] = {cell0.get(), cell1.get()};
+    ASSERT_EQ(avifEncoderAddImageGrid(encoder.get(), kGridCols, kGridRows,
+                                      cells, kAddImageFlags),
+              AVIF_RESULT_OK);
+  } else {
+    ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(),
+                                  kDurationInTimescales, kAddImageFlags),
+              AVIF_RESULT_OK);
+  }
+  ASSERT_EQ(avifEncoderFinish(encoder.get(), &encoded), AVIF_RESULT_OK);
+
+  ImagePtr decoded(avifImageCreateEmpty());
+  ASSERT_NE(decoded, nullptr);
+  DecoderPtr decoder(avifDecoderCreate());
+  ASSERT_NE(decoder, nullptr);
+  decoder->imageContentToDecode = content_to_decode;
+  ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
+                                  encoded.size),
+            content_to_decode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA ||
+                    (create_gainmap &&
+                     content_to_decode & AVIF_IMAGE_CONTENT_GAIN_MAP)
+                ? AVIF_RESULT_OK
+                : AVIF_RESULT_NO_CONTENT);
+
+  if (content_to_decode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) {
+    ASSERT_EQ(image->depth, decoded->depth);
+    ASSERT_EQ(image->width, decoded->width);
+    ASSERT_EQ(image->height, decoded->height);
+    EXPECT_GE(testutil::GetPsnr(*image, *decoded), 20.0);
+  }
+  if (create_gainmap && content_to_decode & AVIF_IMAGE_CONTENT_GAIN_MAP) {
+    ASSERT_NE(image->gainMap, nullptr);
+    ASSERT_NE(image->gainMap->image, nullptr);
+    ASSERT_NE(decoded->gainMap, nullptr);
+    ASSERT_NE(decoded->gainMap->image, nullptr);
+    EXPECT_GE(
+        testutil::GetPsnr(*image->gainMap->image, *decoded->gainMap->image),
+        20.0);
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ImageContentNone, GainmapSampleTransformTest,
+    testing::Combine(
+        testing::Values(AVIF_SAMPLE_TRANSFORM_BIT_DEPTH_EXTENSION_8B_8B),
+        /*create_alpha=*/testing::Values(true),
+        // TODO: b/480081865 - Support gain maps in same file as 'sato' items
+        /*create_gainmap=*/testing::Values(false),
+        /*use_grid=*/testing::Values(true),
+        testing::Values(AVIF_IMAGE_CONTENT_NONE,
+                        AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA,
+                        AVIF_IMAGE_CONTENT_GAIN_MAP, AVIF_IMAGE_CONTENT_ALL)));
+
+//------------------------------------------------------------------------------
+
 }  // namespace
 }  // namespace avif