// Copyright 2022 Google LLC // SPDX-License-Identifier: BSD-2-Clause #include #include #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> {}; // 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> {}; 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(); }