| // Copyright 2022 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include <cstring> |
| #include <tuple> |
| |
| #include "avif/avif.h" |
| #include "avifjpeg.h" |
| #include "avifpng.h" |
| #include "aviftest_helpers.h" |
| #include "gtest/gtest.h" |
| |
| using ::testing::Bool; |
| using ::testing::Combine; |
| using ::testing::Values; |
| |
| namespace libavif { |
| namespace { |
| |
| //------------------------------------------------------------------------------ |
| |
| // Used to pass the data folder path to the GoogleTest suites. |
| const char* data_path = nullptr; |
| |
| //------------------------------------------------------------------------------ |
| // AVIF encode/decode metadata tests |
| |
| class AvifMetadataTest |
| : public testing::TestWithParam< |
| std::tuple</*use_icc=*/bool, /*use_exif=*/bool, /*use_xmp=*/bool>> {}; |
| |
| // Encodes, decodes then verifies that the output metadata matches the input |
| // metadata defined by the parameters. |
| TEST_P(AvifMetadataTest, EncodeDecode) { |
| const bool use_icc = std::get<0>(GetParam()); |
| const bool use_exif = std::get<1>(GetParam()); |
| const bool use_xmp = std::get<2>(GetParam()); |
| |
| testutil::AvifImagePtr image = |
| testutil::CreateImage(/*width=*/12, /*height=*/34, /*depth=*/10, |
| AVIF_PIXEL_FORMAT_YUV444, AVIF_PLANES_ALL); |
| ASSERT_NE(image, nullptr); |
| testutil::FillImageGradient(image.get()); // The pixels do not matter. |
| if (use_icc) { |
| ASSERT_EQ(avifImageSetProfileICC(image.get(), testutil::kSampleIcc.data(), |
| testutil::kSampleIcc.size()), |
| AVIF_RESULT_OK); |
| } |
| if (use_exif) { |
| const avifTransformFlags old_transform_flags = image->transformFlags; |
| const uint8_t old_irot_angle = image->irot.angle; |
| const uint8_t old_imir_axis = image->imir.axis; |
| ASSERT_EQ( |
| avifImageSetMetadataExif(image.get(), testutil::kSampleExif.data(), |
| testutil::kSampleExif.size()), |
| AVIF_RESULT_OK); |
| // testutil::kSampleExif is not a valid Exif payload, just some part of it. |
| // These fields should not be modified. |
| EXPECT_EQ(image->transformFlags, old_transform_flags); |
| EXPECT_EQ(image->irot.angle, old_irot_angle); |
| EXPECT_EQ(image->imir.axis, old_imir_axis); |
| } |
| if (use_xmp) { |
| ASSERT_EQ(avifImageSetMetadataXMP(image.get(), testutil::kSampleXmp.data(), |
| testutil::kSampleXmp.size()), |
| AVIF_RESULT_OK); |
| } |
| |
| // Encode. |
| testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy); |
| ASSERT_NE(encoder, nullptr); |
| encoder->speed = AVIF_SPEED_FASTEST; |
| testutil::AvifRwData encoded_avif; |
| ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded_avif), |
| AVIF_RESULT_OK); |
| |
| // Decode. |
| testutil::AvifImagePtr decoded(avifImageCreateEmpty(), avifImageDestroy); |
| ASSERT_NE(decoded, nullptr); |
| testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), |
| encoded_avif.data, encoded_avif.size), |
| AVIF_RESULT_OK); |
| |
| // Compare input and output metadata. |
| EXPECT_TRUE(testutil::AreByteSequencesEqual( |
| decoded->icc.data, decoded->icc.size, testutil::kSampleIcc.data(), |
| use_icc ? testutil::kSampleIcc.size() : 0u)); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual( |
| decoded->exif.data, decoded->exif.size, testutil::kSampleExif.data(), |
| use_exif ? testutil::kSampleExif.size() : 0u)); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual( |
| decoded->xmp.data, decoded->xmp.size, testutil::kSampleXmp.data(), |
| use_xmp ? testutil::kSampleXmp.size() : 0u)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, AvifMetadataTest, |
| Combine(/*use_icc=*/Bool(), /*use_exif=*/Bool(), |
| /*use_xmp=*/Bool())); |
| |
| //------------------------------------------------------------------------------ |
| // Jpeg and PNG metadata tests |
| |
| testutil::AvifImagePtr WriteAndReadImage(const avifImage& image, |
| const std::string& file_name) { |
| const std::string file_path = testing::TempDir() + file_name; |
| if (file_name.substr(file_name.size() - 4) == ".png") { |
| if (!avifPNGWrite(file_path.c_str(), &image, /*requestedDepth=*/0, |
| AVIF_CHROMA_UPSAMPLING_AUTOMATIC, |
| /*compressionLevel=*/0)) { |
| return {nullptr, nullptr}; |
| } |
| } else { |
| if (!avifJPEGWrite(file_path.c_str(), &image, /*jpegQuality=*/100, |
| AVIF_CHROMA_UPSAMPLING_AUTOMATIC)) { |
| return {nullptr, nullptr}; |
| } |
| } |
| return testutil::ReadImage(testing::TempDir().c_str(), file_name.c_str()); |
| } |
| |
| class MetadataTest : public testing::TestWithParam< |
| std::tuple</*file_name=*/const char*, /*use_icc=*/bool, |
| /*use_exif=*/bool, /*use_xmp=*/bool>> {}; |
| |
| TEST_P(MetadataTest, ReadWriteReadCompare) { |
| const char* file_name = std::get<0>(GetParam()); |
| const bool use_icc = std::get<1>(GetParam()); |
| const bool use_exif = std::get<2>(GetParam()); |
| const bool use_xmp = std::get<3>(GetParam()); |
| |
| const testutil::AvifImagePtr image = testutil::ReadImage( |
| data_path, file_name, AVIF_PIXEL_FORMAT_NONE, 0, |
| AVIF_CHROMA_DOWNSAMPLING_AUTOMATIC, !use_icc, !use_exif, !use_xmp); |
| ASSERT_NE(image, nullptr); |
| EXPECT_NE(image->width * image->height, 0u); |
| |
| if (use_icc) { |
| EXPECT_NE(image->icc.size, 0u); |
| EXPECT_NE(image->icc.data, nullptr); |
| } else { |
| EXPECT_EQ(image->icc.size, 0u); |
| EXPECT_EQ(image->icc.data, nullptr); |
| } |
| if (use_exif) { |
| EXPECT_NE(image->exif.size, 0u); |
| EXPECT_NE(image->exif.data, nullptr); |
| } else { |
| EXPECT_EQ(image->exif.size, 0u); |
| EXPECT_EQ(image->exif.data, nullptr); |
| } |
| if (use_xmp) { |
| EXPECT_NE(image->xmp.size, 0u); |
| EXPECT_NE(image->xmp.data, nullptr); |
| } else { |
| EXPECT_EQ(image->xmp.size, 0u); |
| EXPECT_EQ(image->xmp.data, nullptr); |
| } |
| |
| // Writing and reading that same metadata should give the same bytes. |
| for (const std::string extension : {".png", ".jpg"}) { |
| const testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, file_name + extension); |
| ASSERT_NE(temp_image, nullptr); |
| ASSERT_TRUE(testutil::AreByteSequencesEqual(image->icc, temp_image->icc)); |
| ASSERT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif)); |
| ASSERT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp)); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| PngJpeg, MetadataTest, |
| Combine(Values("paris_icc_exif_xmp.png", // iCCP zTXt zTXt IDAT |
| "paris_icc_exif_xmp_at_end.png", // iCCP IDAT eXIf tEXt |
| "paris_exif_xmp_icc.jpg"), // APP1-Exif, APP1-XMP, APP2-ICC |
| /*use_icc=*/Bool(), /*use_exif=*/Bool(), /*use_xmp=*/Bool())); |
| |
| // Verify all parsers lead exactly to the same metadata bytes. |
| TEST(MetadataTest, Compare) { |
| const testutil::AvifImagePtr ref = |
| testutil::ReadImage(data_path, "paris_icc_exif_xmp.png"); |
| ASSERT_NE(ref, nullptr); |
| EXPECT_GT(ref->exif.size, 0u); |
| EXPECT_GT(ref->xmp.size, 0u); |
| EXPECT_GT(ref->icc.size, 0u); |
| |
| for (const char* file_name : |
| {"paris_exif_xmp_icc.jpg", "paris_icc_exif_xmp_at_end.png"}) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, file_name); |
| ASSERT_NE(image, nullptr); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, ref->exif)); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, ref->xmp)); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->icc, ref->icc)); |
| } |
| } |
| |
| // A test for https://github.com/AOMediaCodec/libavif/issues/1086 to prevent |
| // regression. |
| TEST(MetadataTest, DecoderParseICC) { |
| std::string file_path = std::string(data_path) + "paris_icc_exif_xmp.avif"; |
| avifDecoder* decoder = avifDecoderCreate(); |
| EXPECT_EQ(avifDecoderSetIOFile(decoder, file_path.c_str()), AVIF_RESULT_OK); |
| EXPECT_EQ(avifDecoderParse(decoder), AVIF_RESULT_OK); |
| // Check the first four bytes of the ICC profile. |
| ASSERT_GE(decoder->image->icc.size, 4u); |
| EXPECT_EQ(decoder->image->icc.data[0], 0); |
| EXPECT_EQ(decoder->image->icc.data[1], 0); |
| EXPECT_EQ(decoder->image->icc.data[2], 2); |
| EXPECT_EQ(decoder->image->icc.data[3], 84); |
| avifDecoderDestroy(decoder); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| TEST(MetadataTest, ExifButDefaultIrotImir) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "paris_exif_xmp_icc.jpg"); |
| ASSERT_NE(image, nullptr); |
| // The Exif metadata contains orientation information: 1. |
| // It is converted to no irot/imir. |
| EXPECT_GT(image->exif.size, 0u); |
| EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{AVIF_TRANSFORM_NONE}); |
| |
| const testutil::AvifRwData encoded = |
| testutil::Encode(image.get(), AVIF_SPEED_FASTEST); |
| const testutil::AvifImagePtr decoded = |
| testutil::Decode(encoded.data, encoded.size); |
| ASSERT_NE(decoded, nullptr); |
| |
| // No irot/imir after decoding because 1 maps to default no irot/imir. |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif)); |
| EXPECT_EQ( |
| decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{AVIF_TRANSFORM_NONE}); |
| } |
| |
| TEST(MetadataTest, ExifOrientation) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg"); |
| ASSERT_NE(image, nullptr); |
| // The Exif metadata contains orientation information: 5. |
| EXPECT_GT(image->exif.size, 0u); |
| EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR}); |
| EXPECT_EQ(image->irot.angle, 1u); |
| EXPECT_EQ(image->imir.axis, 0u); |
| |
| const testutil::AvifRwData encoded = |
| testutil::Encode(image.get(), AVIF_SPEED_FASTEST); |
| const testutil::AvifImagePtr decoded = |
| testutil::Decode(encoded.data, encoded.size); |
| ASSERT_NE(decoded, nullptr); |
| |
| // irot/imir are expected. |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif)); |
| EXPECT_EQ( |
| decoded->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR}); |
| EXPECT_EQ(decoded->irot.angle, 1u); |
| EXPECT_EQ(decoded->imir.axis, 0u); |
| |
| // Exif orientation is kept in JPEG export. |
| testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, "paris_exif_orientation_5.jpg"); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif)); |
| EXPECT_EQ(image->transformFlags, temp_image->transformFlags); |
| EXPECT_EQ(image->irot.angle, temp_image->irot.angle); |
| EXPECT_EQ(image->imir.axis, temp_image->imir.axis); |
| EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched. |
| |
| // Exif orientation in PNG export should be ignored or discarded. |
| temp_image = WriteAndReadImage(*image, "paris_exif_orientation_5.png"); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif)); |
| EXPECT_EQ( |
| temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{0}); |
| // TODO(yguyon): Fix orientation not being applied to PNG samples. |
| EXPECT_EQ(image->width, temp_image->width /* should be height here */); |
| } |
| |
| TEST(MetadataTest, ExifOrientationAndForcedImir) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg"); |
| ASSERT_NE(image, nullptr); |
| // The Exif metadata contains orientation information: 5. |
| // Force irot/imir to values that have a different meaning than 5. |
| // This is not recommended but for testing only. |
| EXPECT_GT(image->exif.size, 0u); |
| image->transformFlags = AVIF_TRANSFORM_IMIR; |
| image->imir.axis = 1; |
| |
| const testutil::AvifRwData encoded = |
| testutil::Encode(image.get(), AVIF_SPEED_FASTEST); |
| const testutil::AvifImagePtr decoded = |
| testutil::Decode(encoded.data, encoded.size); |
| ASSERT_NE(decoded, nullptr); |
| |
| // Exif orientation is still there but irot/imir do not match it. |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->exif, decoded->exif)); |
| EXPECT_EQ(decoded->transformFlags, avifTransformFlags{AVIF_TRANSFORM_IMIR}); |
| EXPECT_EQ(decoded->irot.angle, 0u); |
| EXPECT_EQ(decoded->imir.axis, image->imir.axis); |
| |
| // Exif orientation is set equivalent to irot/imir in JPEG export. |
| // Existing Exif orientation is overwritten. |
| const testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, "paris_exif_orientation_2.jpg"); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_FALSE(testutil::AreByteSequencesEqual(image->exif, temp_image->exif)); |
| EXPECT_EQ(image->transformFlags, temp_image->transformFlags); |
| EXPECT_EQ(image->imir.axis, temp_image->imir.axis); |
| EXPECT_EQ(image->width, temp_image->width); // Samples are left untouched. |
| } |
| |
| TEST(MetadataTest, RotatedJpegBecauseOfIrotImir) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "paris_exif_orientation_5.jpg"); |
| ASSERT_NE(image, nullptr); |
| EXPECT_EQ(avifImageSetMetadataExif(image.get(), nullptr, 0), |
| AVIF_RESULT_OK); // Clear Exif. |
| // Orientation is kept in irot/imir. |
| EXPECT_EQ(image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR}); |
| EXPECT_EQ(image->irot.angle, 1u); |
| EXPECT_EQ(image->imir.axis, 0u); |
| |
| // No Exif metadata to store the orientation: the samples should be rotated. |
| const testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, "paris_exif_orientation_5.jpg"); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_EQ(temp_image->exif.size, 0u); |
| EXPECT_EQ( |
| temp_image->transformFlags & (AVIF_TRANSFORM_IROT | AVIF_TRANSFORM_IMIR), |
| avifTransformFlags{0}); |
| // TODO(yguyon): Fix orientation not being applied to JPEG samples. |
| EXPECT_EQ(image->width, temp_image->width /* should be height here */); |
| } |
| |
| TEST(MetadataTest, ExifIfdOffsetLoopingTo8) { |
| const testutil::AvifImagePtr image(avifImageCreateEmpty(), avifImageDestroy); |
| ASSERT_NE(image, nullptr); |
| const uint8_t kBadExifPayload[128] = { |
| 'M', 'M', 0, 42, // TIFF header |
| 0, 0, 0, 8, // Offset to 0th IFD |
| 0, 1, // fieldCount |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // tag, type, count, valueOffset |
| 0, 0, 0, 8 // Invalid IFD offset, infinitely looping back to 0th IFD. |
| }; |
| // avifImageSetMetadataExif() calls |
| // avifImageExtractExifOrientationToIrotImir() internally. |
| // The avifImageExtractExifOrientationToIrotImir() call should not enter an |
| // infinite loop. |
| ASSERT_EQ(avifImageSetMetadataExif( |
| image.get(), kBadExifPayload, |
| sizeof(kBadExifPayload) / sizeof(kBadExifPayload[0])), |
| AVIF_RESULT_OK); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| TEST(MetadataTest, ExtendedXMP) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "dog_exif_extended_xmp_icc.jpg"); |
| ASSERT_NE(image, nullptr); |
| ASSERT_NE(image->xmp.size, 0u); |
| ASSERT_LT(image->xmp.size, |
| size_t{65503}); // Fits in a single JPEG APP1 marker. |
| |
| for (const char* temp_file_name : {"dog.png", "dog.jpg"}) { |
| const testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, temp_file_name); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp)); |
| } |
| } |
| |
| TEST(MetadataTest, MultipleExtendedXMPAndAlternativeGUIDTag) { |
| const testutil::AvifImagePtr image = |
| testutil::ReadImage(data_path, "paris_extended_xmp.jpg"); |
| ASSERT_NE(image, nullptr); |
| ASSERT_GT(image->xmp.size, size_t{65536 * 2}); |
| |
| testutil::AvifImagePtr temp_image = |
| WriteAndReadImage(*image, "paris_extended_xmp.png"); |
| ASSERT_NE(temp_image, nullptr); |
| EXPECT_TRUE(testutil::AreByteSequencesEqual(image->xmp, temp_image->xmp)); |
| |
| // Writing more than 65502 bytes of XMP in a JPEG is not supported. |
| temp_image = WriteAndReadImage(*image, "paris_extended_xmp.jpg"); |
| ASSERT_NE(temp_image, nullptr); |
| ASSERT_EQ(temp_image->xmp.size, 0u); // XMP was dropped. |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| // Regression test for https://github.com/AOMediaCodec/libavif/issues/1333. |
| // Coverage for avifImageFixXMP(). |
| TEST(MetadataTest, XMPWithTrailingNullCharacter) { |
| testutil::AvifImagePtr jpg = |
| testutil::ReadImage(data_path, "paris_xmp_trailing_null.jpg"); |
| ASSERT_NE(jpg, nullptr); |
| ASSERT_NE(jpg->xmp.size, 0u); |
| // avifJPEGRead() should strip the trailing null character. |
| ASSERT_EQ(std::memchr(jpg->xmp.data, '\0', jpg->xmp.size), nullptr); |
| |
| // Append a zero byte to see what happens when encoded with libpng. |
| ASSERT_EQ(avifRWDataRealloc(&jpg->xmp, jpg->xmp.size + 1), AVIF_RESULT_OK); |
| jpg->xmp.data[jpg->xmp.size - 1] = '\0'; |
| testutil::WriteImage(jpg.get(), |
| (testing::TempDir() + "xmp_trailing_null.png").c_str()); |
| const testutil::AvifImagePtr png = |
| testutil::ReadImage(testing::TempDir().c_str(), "xmp_trailing_null.png"); |
| ASSERT_NE(png, nullptr); |
| ASSERT_NE(png->xmp.size, 0u); |
| // avifPNGRead() should strip the trailing null character, but the libpng |
| // export during testutil::WriteImage() probably took care of that anyway. |
| ASSERT_EQ(std::memchr(png->xmp.data, '\0', png->xmp.size), nullptr); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| } // namespace |
| } // namespace libavif |
| |
| 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; |
| } |
| libavif::data_path = argv[1]; |
| return RUN_ALL_TESTS(); |
| } |