diff --git a/CHANGELOG.md b/CHANGELOG.md index b73863936b..b8867688da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,14 +20,19 @@ The changes are relative to the previous release, unless the baseline is specifi 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 d937aebfe0..08cff7b95f 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c @@ -34,6 +34,7 @@ static void syntax(void) 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 @@ int main(int argc, char * argv[]) 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 @@ int main(int argc, char * argv[]) 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 @@ int main(int argc, char * argv[]) 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 204a96ee05..5650c80990 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -1234,6 +1234,11 @@ typedef enum avifImageContentTypeFlag 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 7789541bdc..2c16c0f0ec 100644 --- a/src/read.c +++ b/src/read.c @@ -6242,7 +6242,8 @@ avifResult avifDecoderReset(avifDecoder * decoder) // 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 @@ avifResult avifDecoderReset(avifDecoder * decoder) 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 @@ avifResult avifDecoderNextImage(avifDecoder * decoder) // 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 d6151a61ed..d5223384a2 100644 --- a/tests/gtest/avif16bittest.cc +++ b/tests/gtest/avif16bittest.cc @@ -83,8 +83,15 @@ TEST_P(SampleTransformTest, Avif16bit) { } 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 @@ TEST_P(SampleTransformTest, Avif16bit) { 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 @@ TEST_P(GainmapSampleTransformTest, ImageContentToDecode) { 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 @@ INSTANTIATE_TEST_SUITE_P( // 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 a8299f633a..2bdb7d1fe1 100644 --- a/tests/gtest/avif_fuzztest_dec_incr.cc +++ b/tests/gtest/avif_fuzztest_dec_incr.cc @@ -117,7 +117,8 @@ FUZZ_TEST(DecodeAvifFuzzTest, DecodeIncr) /*give_size_hint=*/Arbitrary(), fuzztest::BitFlagCombinationOf( {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()); //------------------------------------------------------------------------------ diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h index 827c2bdb58..0932e01218 100644 --- a/tests/gtest/avif_fuzztest_helpers.h +++ b/tests/gtest/avif_fuzztest_helpers.h @@ -297,7 +297,11 @@ inline auto ArbitraryAvifDecoderWithGainMapOptions() { AddGainMapOptionsToDecoder, ArbitraryBaseAvifDecoder(), fuzztest::ElementOf( {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 f1c1a0139a..ee96515d88 100644 --- a/tests/gtest/avifaltrtest.cc +++ b/tests/gtest/avifaltrtest.cc @@ -39,6 +39,7 @@ TEST(AltrTest, SampleTransformDepthEqualToInput) { 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 @@ TEST(AltrTest, SampleTransformDepthParseNextEqual) { 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 @@ TEST(AltrTest, ZeroImageContentToDecode) { 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 ec8ca68c6d..699f01a190 100644 --- a/tests/gtest/avifdecodetest.cc +++ b/tests/gtest/avifdecodetest.cc @@ -40,19 +40,28 @@ TEST(AvifDecodeTest, ImageContentToDecodeNone) { 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); + } } }