| // Copyright 2023 Google LLC | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <vector> | |
| #include "avif/avif.h" | |
| #include "aviftest_helpers.h" | |
| #include "gtest/gtest.h" | |
| namespace avif { | |
| namespace { | |
| // Used to pass the data folder path to the GoogleTest suites. | |
| const char* data_path = nullptr; | |
| TEST(AvifDecodeTest, AnimatedImage) { | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip test."; | |
| } | |
| // Both files should give exactly the same result: the audio is ignored | |
| // in 'colors-animated-8bpc-audio.avif' but shouldn't prevent the file from | |
| // decoding. | |
| for (const char* file_name : | |
| {"colors-animated-8bpc.avif", "colors-animated-8bpc-audio.avif"}) { | |
| SCOPED_TRACE(file_name); | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| 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->alphaPresent, AVIF_FALSE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| EXPECT_EQ(decoder->repetitionCount, 0); | |
| for (int i = 0; i < 5; ++i) { | |
| EXPECT_EQ(avifDecoderIsKeyframe(decoder.get(), i), i == 0); | |
| EXPECT_EQ(avifDecoderNearestKeyframe(decoder.get(), i), 0); | |
| } | |
| for (int i = 0; i < 5; ++i) { | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| } | |
| } | |
| } | |
| TEST(AvifDecodeTest, AnimatedImageWithSourceSetToPrimaryItem) { | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip test."; | |
| } | |
| const char* file_name = "colors-animated-8bpc.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), | |
| (std::string(data_path) + file_name).c_str()), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ( | |
| avifDecoderSetSource(decoder.get(), AVIF_DECODER_SOURCE_PRIMARY_ITEM), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_FALSE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| // imageCount is expected to be 1 because we are using primary item as the | |
| // preferred source. | |
| EXPECT_EQ(decoder->imageCount, 1); | |
| EXPECT_EQ(decoder->repetitionCount, 0); | |
| // Get the first (and only) image. | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| // Subsequent calls should not return AVIF_RESULT_OK since there is only one | |
| // image in the preferred source. | |
| EXPECT_NE(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| } | |
| TEST(AvifDecodeTest, AnimatedImageWithAlphaAndMetadata) { | |
| const char* file_name = "colors-animated-8bpc-alpha-exif-xmp.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), | |
| (std::string(data_path) + file_name).c_str()), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| EXPECT_EQ(decoder->repetitionCount, AVIF_REPETITION_COUNT_INFINITE); | |
| EXPECT_EQ(decoder->image->exif.size, 1126); | |
| EXPECT_EQ(decoder->image->xmp.size, 3898); | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip the rest of the test."; | |
| } | |
| for (int i = 0; i < 5; ++i) { | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_NE(decoder->image->alphaPlane, nullptr); | |
| EXPECT_GT(decoder->image->alphaRowBytes, 0); | |
| } | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), | |
| AVIF_RESULT_NO_IMAGES_REMAINING); | |
| } | |
| //------------------------------------------------------------------------------ | |
| // The alpha track of colors-animated-8bpc-alpha-exif-xmp.avif holds a 20 byte | |
| // 'tref' box at offset 1586, with a single 'auxl' box naming track 1, directly | |
| // followed by a 44 byte 'edts' box. Section 8.3.3.2 of ISO/IEC 14496-12 gives | |
| // TrackReferenceTypeBox an array of track_IDs, and nothing limits a 'tref' to | |
| // one box of a given type. The cases below rewrite those 64 bytes in place and | |
| // pad with a 'free' box, so the file size and every absolute offset in the file | |
| // keep the values of the valid file. | |
| constexpr const char* kAlphaTrackFileName = | |
| "colors-animated-8bpc-alpha-exif-xmp.avif"; | |
| constexpr size_t kTrefOffset = 1586; | |
| constexpr size_t kTrefSize = 20; | |
| constexpr size_t kTrefAndEdtsSize = 64; | |
| void WriteBE32(uint32_t value, uint8_t* bytes) { | |
| bytes[0] = static_cast<uint8_t>(value >> 24); | |
| bytes[1] = static_cast<uint8_t>(value >> 16); | |
| bytes[2] = static_cast<uint8_t>(value >> 8); | |
| bytes[3] = static_cast<uint8_t>(value); | |
| } | |
| void AppendTrackReferenceTypeBox(const char* type, | |
| const std::vector<uint32_t>& track_ids, | |
| std::vector<uint8_t>* bytes) { | |
| const size_t size = 8 + 4 * track_ids.size(); | |
| const size_t offset = bytes->size(); | |
| bytes->resize(offset + size); | |
| WriteBE32(static_cast<uint32_t>(size), bytes->data() + offset); | |
| std::memcpy(bytes->data() + offset + 4, type, 4); | |
| for (size_t i = 0; i < track_ids.size(); ++i) { | |
| WriteBE32(track_ids[i], bytes->data() + offset + 8 + 4 * i); | |
| } | |
| } | |
| // Overwrites the 'tref' and 'edts' boxes of the alpha track with |boxes| and | |
| // fills the rest of the 64 bytes with a 'free' box. | |
| void ReplaceTrefAndEdts(const std::vector<uint8_t>& boxes, | |
| testutil::AvifRwData* encoded) { | |
| ASSERT_GE(encoded->size, kTrefOffset + kTrefAndEdtsSize); | |
| uint8_t* p = encoded->data + kTrefOffset; | |
| // Sanity check: the fixture is the file these tests were authored against. | |
| ASSERT_EQ(std::memcmp(p + 4, "tref", 4), 0); | |
| ASSERT_EQ(std::memcmp(p + kTrefSize + 4, "edts", 4), 0); | |
| ASSERT_LE(boxes.size() + 8, kTrefAndEdtsSize); | |
| std::copy(boxes.begin(), boxes.end(), p); | |
| uint8_t* free_box = p + boxes.size(); | |
| const size_t free_size = kTrefAndEdtsSize - boxes.size(); | |
| WriteBE32(static_cast<uint32_t>(free_size), free_box); | |
| std::memcpy(free_box + 4, "free", 4); | |
| std::memset(free_box + 8, 0, free_size - 8); | |
| } | |
| void ExpectAlphaTrackIsFound(const testutil::AvifRwData& encoded) { | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), encoded.data, encoded.size), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| } | |
| // Control for the two cases below. The alpha track keeps its single reference, | |
| // only its 'edts' box is replaced by a 'free' box. Dropping it leaves that | |
| // track's repetition count unknown, which the decoder takes from the color | |
| // track anyway, so the alpha track still has to be found. | |
| TEST(AvifDecodeTest, AlphaTrackWithoutEdts) { | |
| testutil::AvifRwData encoded = | |
| testutil::ReadFile(std::string(data_path) + kAlphaTrackFileName); | |
| ASSERT_NE(encoded.size, size_t{0}); | |
| std::vector<uint8_t> tref; | |
| AppendTrackReferenceTypeBox("auxl", {1}, &tref); | |
| std::vector<uint8_t> boxes; | |
| AppendTrackReferenceTypeBox("tref", {}, &boxes); | |
| WriteBE32(static_cast<uint32_t>(8 + tref.size()), boxes.data()); | |
| boxes.insert(boxes.end(), tref.begin(), tref.end()); | |
| ASSERT_NO_FATAL_FAILURE(ReplaceTrefAndEdts(boxes, &encoded)); | |
| ExpectAlphaTrackIsFound(encoded); | |
| } | |
| // The 'tref' box holds two 'auxl' boxes. Check that both are read. | |
| TEST(AvifDecodeTest, AlphaTrackWithTwoAuxlBoxes) { | |
| testutil::AvifRwData encoded = | |
| testutil::ReadFile(std::string(data_path) + kAlphaTrackFileName); | |
| ASSERT_NE(encoded.size, size_t{0}); | |
| std::vector<uint8_t> children; | |
| AppendTrackReferenceTypeBox("auxl", {1}, &children); | |
| AppendTrackReferenceTypeBox("auxl", {99}, &children); | |
| std::vector<uint8_t> boxes; | |
| AppendTrackReferenceTypeBox("tref", {}, &boxes); | |
| WriteBE32(static_cast<uint32_t>(8 + children.size()), boxes.data()); | |
| boxes.insert(boxes.end(), children.begin(), children.end()); | |
| ASSERT_NO_FATAL_FAILURE(ReplaceTrefAndEdts(boxes, &encoded)); | |
| ExpectAlphaTrackIsFound(encoded); | |
| } | |
| // One 'auxl' box names two tracks. Check that both are read. | |
| TEST(AvifDecodeTest, AlphaTrackWithTwoTrackIds) { | |
| testutil::AvifRwData encoded = | |
| testutil::ReadFile(std::string(data_path) + kAlphaTrackFileName); | |
| ASSERT_NE(encoded.size, size_t{0}); | |
| std::vector<uint8_t> children; | |
| AppendTrackReferenceTypeBox("auxl", {99, 1}, &children); | |
| std::vector<uint8_t> boxes; | |
| AppendTrackReferenceTypeBox("tref", {}, &boxes); | |
| WriteBE32(static_cast<uint32_t>(8 + children.size()), boxes.data()); | |
| boxes.insert(boxes.end(), children.begin(), children.end()); | |
| ASSERT_NO_FATAL_FAILURE(ReplaceTrefAndEdts(boxes, &encoded)); | |
| ExpectAlphaTrackIsFound(encoded); | |
| } | |
| //------------------------------------------------------------------------------ | |
| TEST(AvifDecodeTest, AnimatedImageWithAlphaAndMetadataIgnoreAlpha) { | |
| const char* file_name = "colors-animated-8bpc-alpha-exif-xmp.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| decoder->imageContentToDecode &= ~AVIF_IMAGE_CONTENT_ALPHA; | |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), | |
| (std::string(data_path) + file_name).c_str()), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| EXPECT_EQ(decoder->repetitionCount, AVIF_REPETITION_COUNT_INFINITE); | |
| EXPECT_EQ(decoder->image->exif.size, 1126); | |
| EXPECT_EQ(decoder->image->xmp.size, 3898); | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip the rest of the test."; | |
| } | |
| for (int i = 0; i < 5; ++i) { | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->image->alphaPlane, nullptr); | |
| EXPECT_EQ(decoder->image->alphaRowBytes, 0); | |
| } | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), | |
| AVIF_RESULT_NO_IMAGES_REMAINING); | |
| } | |
| TEST(AvifDecodeTest, AnimatedImageWithAlphaAndMetadataIgnoreAll) { | |
| const char* file_name = "colors-animated-8bpc-alpha-exif-xmp.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| 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); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| EXPECT_EQ(decoder->repetitionCount, AVIF_REPETITION_COUNT_INFINITE); | |
| EXPECT_EQ(decoder->image->exif.size, 1126); | |
| EXPECT_EQ(decoder->image->xmp.size, 3898); | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip the rest of the test."; | |
| } | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_NO_CONTENT); | |
| } | |
| TEST(AvifDecodeTest, AnimatedImageWithDepthAndMetadata) { | |
| // Depth is not supported and should be ignored. | |
| const char* file_name = "colors-animated-8bpc-depth-exif-xmp.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), | |
| (std::string(data_path) + file_name).c_str()), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_FALSE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| EXPECT_EQ(decoder->imageCount, 5); | |
| EXPECT_EQ(decoder->repetitionCount, AVIF_REPETITION_COUNT_INFINITE); | |
| EXPECT_EQ(decoder->image->exif.size, 1126); | |
| EXPECT_EQ(decoder->image->xmp.size, 3898); | |
| } | |
| TEST(AvifDecodeTest, | |
| AnimatedImageWithDepthAndMetadataWithSourceSetToPrimaryItem) { | |
| if (!testutil::Av1DecoderAvailable()) { | |
| GTEST_SKIP() << "AV1 Codec unavailable, skip test."; | |
| } | |
| // Depth is not supported and should be ignored. | |
| const char* file_name = "colors-animated-8bpc-depth-exif-xmp.avif"; | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), | |
| (std::string(data_path) + file_name).c_str()), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ( | |
| avifDecoderSetSource(decoder.get(), AVIF_DECODER_SOURCE_PRIMARY_ITEM), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); | |
| EXPECT_EQ(decoder->alphaPresent, AVIF_FALSE); | |
| EXPECT_EQ(decoder->imageSequenceTrackPresent, AVIF_TRUE); | |
| // imageCount is expected to be 1 because we are using primary item as the | |
| // preferred source. | |
| EXPECT_EQ(decoder->imageCount, 1); | |
| EXPECT_EQ(decoder->repetitionCount, 0); | |
| EXPECT_EQ(decoder->image->exif.size, 1126); | |
| EXPECT_EQ(decoder->image->xmp.size, 3898); | |
| // Get the first (and only) image. | |
| EXPECT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| // Subsequent calls should not return AVIF_RESULT_OK since there is only one | |
| // image in the preferred source. | |
| EXPECT_NE(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK); | |
| } | |
| TEST(AvifDecodeTest, AnimatedImageWithoutTracksShouldFail) { | |
| testutil::AvifRwData avif = | |
| testutil::ReadFile(std::string(data_path) + "colors-animated-8bpc.avif"); | |
| // Edit the file to replace 'trak' box with a 'free' box. This way the file | |
| // will not contain any 'trak' boxes. | |
| const uint8_t* kTrak = reinterpret_cast<const uint8_t*>("trak"); | |
| uint8_t* trak_position = | |
| std::search(avif.data, avif.data + avif.size, kTrak, kTrak + 4); | |
| ASSERT_NE(trak_position, avif.data + avif.size); | |
| trak_position[0] = static_cast<uint8_t>('f'); | |
| trak_position[1] = static_cast<uint8_t>('r'); | |
| trak_position[2] = static_cast<uint8_t>('e'); | |
| trak_position[3] = static_cast<uint8_t>('e'); | |
| for (auto source : | |
| {AVIF_DECODER_SOURCE_PRIMARY_ITEM, AVIF_DECODER_SOURCE_TRACKS}) { | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| ASSERT_NE(decoder, nullptr); | |
| ASSERT_EQ(avifDecoderSetIOMemory(decoder.get(), avif.data, avif.size), | |
| AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderSetSource(decoder.get(), source), AVIF_RESULT_OK); | |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_BMFF_PARSE_FAILED); | |
| } | |
| } | |
| } // namespace | |
| } // namespace avif | |
| int main(int argc, char** argv) { | |
| ::testing::InitGoogleTest(&argc, argv); | |
| if (argc != 2) { | |
| std::cerr << "There must be exactly one argument containing the path to " | |
| "the test data folder" | |
| << std::endl; | |
| return 1; | |
| } | |
| avif::data_path = argv[1]; | |
| return RUN_ALL_TESTS(); | |
| } |