Disable Sample Transform decoding by default (#3018)

Add AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS.
Add --sato flag to avifdec.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b738639..b886768 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,14 +20,19 @@
   a cICP chunk and other color information chunks, such as iCCP (ICC profile),
   the other chunks are ignored as per the PNG Specification Third Edition
   Section 4.3.
+* Support reading Sample-Transform-based 16-bit AVIF files when
+  avifDecoder::imageContentToDecode & AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS is
+  not zero.
 * Support Sample Transform derived image items with grid input image items.
+* Add --sato flag to avifdec to enable Sample Transforms support at decoding.
 * Add --grid option to avifgainmaputil.
 
 ### Changed since 1.3.0
 
 * Set avifDecoder::image->depth to the same value after avifDecoderParse() as
-  after avifDecoderNextImage() when AVIF_ENABLE_EXPERIMENTAL_SAMPLE_TRANSFORM is
-  enabled and when the file to decode contains a 'sato' derived image item.
+  after avifDecoderNextImage() when the file to decode contains a 'sato' derived
+  image item.
+* avifdec only enables Sample Transform decoding when --depth is set to 16.
 * Update dav1d.cmd/dav1d_android.sh/LocalDav1d.cmake: 1.5.3
 * Update googletest.cmd/LocalGTest.cmake: v1.17.0
 * Update libjpeg.cmd/LocalJpeg.cmake: 3.1.3
diff --git a/apps/avifdec.c b/apps/avifdec.c
index d937aeb..08cff7b 100644
--- a/apps/avifdec.c
+++ b/apps/avifdec.c
@@ -34,6 +34,7 @@
     printf("    -j,--jobs J       : Number of jobs (worker threads), or 'all' to potentially use as many cores as possible. (Default: all)\n");
     printf("    -c,--codec C      : Codec to use (choose from versions list below)\n");
     printf("    -d,--depth D      : Output depth, either 8 or 16. (PNG only; For y4m, depth is retained, and JPEG is always 8bpc)\n");
+    printf("    --sato            : Enable Sample Transforms decoding (e.g. 16-bit AVIF)\n");
     printf("    -q,--quality Q    : Output quality in 0..100. (JPEG only, default: %d)\n", DEFAULT_JPEG_QUALITY);
     printf("    --png-compress L  : PNG compression level in 0..9 (PNG only; 0=none, 9=max). Defaults to libpng's builtin default\n");
     printf("    -u,--upsampling U : Chroma upsampling (for 420/422). One of 'automatic' (default), 'fastest', 'best', 'nearest', or 'bilinear'\n");
@@ -88,6 +89,7 @@
     const char * inputFilename = NULL;
     const char * outputFilename = NULL;
     int requestedDepth = 0;
+    avifBool enableSampleTransforms = AVIF_FALSE;
     int jobs = -1;
     int jpegQuality = DEFAULT_JPEG_QUALITY;
     int pngCompressionLevel = -1; // -1 is a sentinel to avifPNGWrite() to skip calling png_set_compression_level()
@@ -168,6 +170,8 @@
                 fprintf(stderr, "ERROR: invalid depth: %s\n", arg);
                 return 1;
             }
+        } else if (!strcmp(arg, "--sato")) {
+            enableSampleTransforms = AVIF_TRUE;
         } else if (!strcmp(arg, "-q") || !strcmp(arg, "--quality")) {
             NEXTARG();
             jpegQuality = atoi(arg);
@@ -312,7 +316,9 @@
     decoder->strictFlags = strictFlags;
     decoder->allowProgressive = allowProgressive;
     if (infoOnly) {
-        decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_ALL;
+        decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP | AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
+    } else if (enableSampleTransforms) {
+        decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
     }
 
     avifResult result = avifDecoderSetIOFile(decoder, inputFilename);
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 204a96e..5650c80 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -1234,6 +1234,11 @@
     AVIF_IMAGE_CONTENT_GAIN_MAP = (1 << 2),
     AVIF_IMAGE_CONTENT_ALL = AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP,
 
+    // Mostly used for bit depth extensions to go beyond the underlying codec capability
+    // (e.g. 16-bit AVIF). Not part of AVIF_IMAGE_CONTENT_ALL as this is a rare use case.
+    // Has no effect without AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA.
+    AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS = (1 << 3),
+
     AVIF_IMAGE_CONTENT_DECODE_DEFAULT = AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA,
 } avifImageContentTypeFlag;
 typedef uint32_t avifImageContentTypeFlags;
diff --git a/src/read.c b/src/read.c
index 7789541..2c16c0f 100644
--- a/src/read.c
+++ b/src/read.c
@@ -6242,7 +6242,8 @@
 
         // AVIF_ITEM_SAMPLE_TRANSFORM (not used through mainItems because not a coded item (well grids are not coded items either but it's different)).
         avifDecoderItem * const sampleTransformItem = avifDecoderDataFindSampleTransformImageItem(data);
-        if (sampleTransformItem != NULL) {
+        if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) &&
+            (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS) && sampleTransformItem != NULL) {
             AVIF_ASSERT_OR_RETURN(data->sampleTransformNumInputImageItems == 0);
 
             for (uint32_t i = 0; i < data->meta->items.count; ++i) {
@@ -6367,12 +6368,14 @@
 
             AVIF_CHECKRES(avifDecoderAdoptGridTileCodecTypeIfNeeded(decoder, mainItems[c], &data->tileInfos[c]));
 
-            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 (c == AVIF_ITEM_COLOR || c == AVIF_ITEM_ALPHA) {
                 if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA)) {
                     continue;
                 }
+            } else if (c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_COLOR ||
+                       c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_ALPHA) {
+                AVIF_ASSERT_OR_RETURN((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) &&
+                                      (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS));
             } else {
                 AVIF_ASSERT_OR_RETURN(c == AVIF_ITEM_GAIN_MAP);
                 if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_GAIN_MAP)) {
@@ -6942,7 +6945,6 @@
     // decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA was equal to 0.
     // Only apply Sample Transforms if there is a color item to apply it onto.
     if (decoder->data->tileInfos[AVIF_ITEM_COLOR].tileCount != 0 && 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/tests/gtest/avif16bittest.cc b/tests/gtest/avif16bittest.cc
index d6151a6..d522338 100644
--- a/tests/gtest/avif16bittest.cc
+++ b/tests/gtest/avif16bittest.cc
@@ -83,8 +83,15 @@
   }
   ASSERT_EQ(avifEncoderFinish(encoder.get(), &encoded), AVIF_RESULT_OK);
 
-  const ImagePtr decoded = testutil::Decode(encoded.data, encoded.size);
+  ImagePtr decoded(avifImageCreateEmpty());
   ASSERT_NE(decoded, nullptr);
+  DecoderPtr decoder(avifDecoderCreate());
+  ASSERT_NE(decoder, nullptr);
+  decoder->imageContentToDecode =
+      AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
+  ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
+                                  encoded.size),
+            AVIF_RESULT_OK);
 
   ASSERT_EQ(image->depth, decoded->depth);
   ASSERT_EQ(image->width, decoded->width);
@@ -100,8 +107,14 @@
       std::memcpy(&encoded.data[i], "zzzz", 4);
     }
   }
-  const ImagePtr decoded_no_sato = testutil::Decode(encoded.data, encoded.size);
+  ImagePtr decoded_no_sato(avifImageCreateEmpty());
   ASSERT_NE(decoded_no_sato, nullptr);
+  DecoderPtr decoder_no_sato(avifDecoderCreate());
+  ASSERT_NE(decoder_no_sato, nullptr);
+  decoder_no_sato->imageContentToDecode = decoder->imageContentToDecode;
+  ASSERT_EQ(avifDecoderReadMemory(decoder_no_sato.get(), decoded_no_sato.get(),
+                                  encoded.data, encoded.size),
+            AVIF_RESULT_OK);
   // Only the most significant bits of each sample can be retrieved.
   // They should be encoded losslessly no matter the quantizer settings.
   ImagePtr image_no_sato = testutil::CreateImage(
@@ -297,12 +310,13 @@
   ASSERT_NE(decoded, nullptr);
   DecoderPtr decoder(avifDecoderCreate());
   ASSERT_NE(decoder, nullptr);
-  decoder->imageContentToDecode = content_to_decode;
+  decoder->imageContentToDecode =
+      content_to_decode | AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
   ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data,
                                   encoded.size),
-            content_to_decode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA ||
+            (content_to_decode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) ||
                     (create_gainmap &&
-                     content_to_decode & AVIF_IMAGE_CONTENT_GAIN_MAP)
+                     (content_to_decode & AVIF_IMAGE_CONTENT_GAIN_MAP))
                 ? AVIF_RESULT_OK
                 : AVIF_RESULT_NO_CONTENT);
 
@@ -331,6 +345,7 @@
         // Gain maps are not supported in the same file as 'sato' items.
         /*create_gainmap=*/testing::Values(false),
         /*use_grid=*/testing::Values(true),
+        // AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS is always set in this test.
         testing::Values(AVIF_IMAGE_CONTENT_NONE,
                         AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA,
                         AVIF_IMAGE_CONTENT_GAIN_MAP, AVIF_IMAGE_CONTENT_ALL)));
diff --git a/tests/gtest/avif_fuzztest_dec_incr.cc b/tests/gtest/avif_fuzztest_dec_incr.cc
index a8299f6..2bdb7d1 100644
--- a/tests/gtest/avif_fuzztest_dec_incr.cc
+++ b/tests/gtest/avif_fuzztest_dec_incr.cc
@@ -117,7 +117,8 @@
                  /*give_size_hint=*/Arbitrary<bool>(),
                  fuzztest::BitFlagCombinationOf<avifImageContentTypeFlags>(
                      {AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA,
-                      AVIF_IMAGE_CONTENT_GAIN_MAP}),
+                      AVIF_IMAGE_CONTENT_GAIN_MAP,
+                      AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS}),
                  /*use_nth_image_api=*/Arbitrary<bool>());
 
 //------------------------------------------------------------------------------
diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h
index 827c2bd..0932e01 100644
--- a/tests/gtest/avif_fuzztest_helpers.h
+++ b/tests/gtest/avif_fuzztest_helpers.h
@@ -297,7 +297,11 @@
       AddGainMapOptionsToDecoder, ArbitraryBaseAvifDecoder(),
       fuzztest::ElementOf<avifImageContentTypeFlags>(
           {AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA,
-           AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP}));
+           AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP,
+           AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA |
+               AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS,
+           AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP |
+               AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS}));
 }
 
 // Generator for an arbitrary DecoderPtr.
diff --git a/tests/gtest/avifaltrtest.cc b/tests/gtest/avifaltrtest.cc
index f1c1a01..ee96515 100644
--- a/tests/gtest/avifaltrtest.cc
+++ b/tests/gtest/avifaltrtest.cc
@@ -39,6 +39,7 @@
 
   DecoderPtr decoder(avifDecoderCreate());
   ASSERT_NE(decoder, nullptr);
+  decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
   ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size),
             AVIF_RESULT_OK);
   ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
@@ -59,6 +60,7 @@
 
   DecoderPtr decoder(avifDecoderCreate());
   ASSERT_NE(decoder, nullptr);
+  decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
   ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size),
             AVIF_RESULT_OK);
 
@@ -84,6 +86,20 @@
             AVIF_RESULT_NO_CONTENT);
 }
 
+TEST(AltrTest, OnlySampleTranformContentToDecode) {
+  const std::string file_path =
+      std::string(data_path) + "weld_sato_12B_8B_q0.avif";
+
+  DecoderPtr decoder(avifDecoderCreate());
+  ASSERT_NE(decoder, nullptr);
+  // Has no effect without AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA.
+  decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS;
+  ImagePtr image(avifImageCreateEmpty());
+  ASSERT_NE(image, nullptr);
+  ASSERT_EQ(avifDecoderReadFile(decoder.get(), image.get(), file_path.c_str()),
+            AVIF_RESULT_NO_CONTENT);
+}
+
 //------------------------------------------------------------------------------
 
 }  // namespace
diff --git a/tests/gtest/avifdecodetest.cc b/tests/gtest/avifdecodetest.cc
index ec8ca68..699f01a 100644
--- a/tests/gtest/avifdecodetest.cc
+++ b/tests/gtest/avifdecodetest.cc
@@ -40,19 +40,28 @@
   for (const std::string file_name :
        {"paris_icc_exif_xmp.avif", "draw_points_idat.avif",
         "sofa_grid1x5_420.avif", "color_grid_alpha_nogrid.avif",
-        "seine_sdr_gainmap_srgb.avif", "draw_points_idat_progressive.avif"}) {
-    SCOPED_TRACE(file_name);
-    DecoderPtr decoder(avifDecoderCreate());
-    ASSERT_NE(decoder, nullptr);
-    // Do not decode anything.
-    decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_NONE;
-    ASSERT_EQ(avifDecoderSetIOFile(
-                  decoder.get(), (std::string(data_path) + file_name).c_str()),
-              AVIF_RESULT_OK);
-    ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK)
-        << decoder->diag.error;
-    EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_FALSE);
-    EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_NO_CONTENT);
+        "seine_sdr_gainmap_srgb.avif", "draw_points_idat_progressive.avif",
+        "weld_sato_12B_8B_q0.avif"}) {
+    for (
+        avifImageContentTypeFlag image_content_to_decode : {
+            AVIF_IMAGE_CONTENT_NONE,
+            AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS  // Equivalent to NONE without
+                                                  // AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA.
+        }) {
+      SCOPED_TRACE(file_name);
+      DecoderPtr decoder(avifDecoderCreate());
+      ASSERT_NE(decoder, nullptr);
+      // Do not decode anything.
+      decoder->imageContentToDecode = image_content_to_decode;
+      ASSERT_EQ(
+          avifDecoderSetIOFile(decoder.get(),
+                               (std::string(data_path) + file_name).c_str()),
+          AVIF_RESULT_OK);
+      ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK)
+          << decoder->diag.error;
+      EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_FALSE);
+      EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_NO_CONTENT);
+    }
   }
 }