| // Copyright 2025 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #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(TransformTest, ClapIrotImir) { |
| if (!testutil::Av1EncoderAvailable() || !testutil::Av1DecoderAvailable()) { |
| GTEST_SKIP() << "AV1 codec unavailable, skip test."; |
| } |
| |
| avifDiagnostics diag{}; |
| ImagePtr 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. |
| |
| image->transformFlags |= AVIF_TRANSFORM_CLAP; |
| const avifCropRect rect{/*x=*/4, /*y=*/6, /*width=*/8, /*height=*/10}; |
| ASSERT_TRUE(avifCleanApertureBoxFromCropRect( |
| &image->clap, &rect, image->width, image->height, &diag)); |
| |
| image->transformFlags |= AVIF_TRANSFORM_IROT; |
| image->irot.angle = 1; |
| image->transformFlags |= AVIF_TRANSFORM_IMIR; |
| image->imir.axis = 1; |
| |
| // Encode. |
| EncoderPtr encoder(avifEncoderCreate()); |
| ASSERT_NE(encoder, nullptr); |
| encoder->speed = AVIF_SPEED_FASTEST; |
| testutil::AvifRwData encoded; |
| ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), |
| AVIF_RESULT_OK); |
| |
| // Decode. |
| ImagePtr decoded(avifImageCreateEmpty()); |
| ASSERT_NE(decoded, nullptr); |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, |
| encoded.size), |
| AVIF_RESULT_OK); |
| |
| EXPECT_EQ(decoded->transformFlags, image->transformFlags); |
| EXPECT_EQ(decoded->clap.widthN, image->clap.widthN); |
| EXPECT_EQ(decoded->clap.widthD, image->clap.widthD); |
| EXPECT_EQ(decoded->clap.heightN, image->clap.heightN); |
| EXPECT_EQ(decoded->clap.heightD, image->clap.heightD); |
| EXPECT_EQ(decoded->clap.horizOffN, image->clap.horizOffN); |
| EXPECT_EQ(decoded->clap.horizOffD, image->clap.horizOffD); |
| EXPECT_EQ(decoded->clap.vertOffN, image->clap.vertOffN); |
| EXPECT_EQ(decoded->clap.vertOffD, image->clap.vertOffD); |
| EXPECT_EQ(decoded->irot.angle, image->irot.angle); |
| EXPECT_EQ(decoded->imir.axis, image->imir.axis); |
| } |
| |
| TEST(TransformTest, ClapIrotImirNonEssential) { |
| // Invalid file with non-essential transformative properties. |
| const std::string path = |
| std::string(data_path) + "clap_irot_imir_non_essential.avif"; |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), path.c_str()), AVIF_RESULT_OK); |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_BMFF_PARSE_FAILED); |
| } |
| |
| TEST(TransformTest, ClopIrotImor) { |
| // File with a non-essential unrecognized property 'clop', an essential |
| // transformation property 'irot', and a non-essential unrecognized property |
| // 'imor'. |
| const std::string path = std::string(data_path) + "clop_irot_imor.avif"; |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderSetIOFile(decoder.get(), path.c_str()), AVIF_RESULT_OK); |
| ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK); |
| |
| // 'imor' should be ignored as it is after a transformative property in the |
| // 'ipma' association order. libavif still surfaces it because this constraint |
| // is relaxed in Amd2 of HEIF ISO/IEC 23008-12. |
| // See https://github.com/MPEGGroup/FileFormat/issues/113. |
| ASSERT_EQ(decoder->image->numProperties, 2u); |
| const avifImageItemProperty& clop = decoder->image->properties[0]; |
| EXPECT_EQ(std::string(clop.boxtype, clop.boxtype + 4), "clop"); |
| const avifImageItemProperty& imor = decoder->image->properties[1]; |
| EXPECT_EQ(std::string(imor.boxtype, imor.boxtype + 4), "imor"); |
| } |
| |
| TEST(TransformTest, IrotAlphaEncodedDecoded) { |
| if (!testutil::Av1EncoderAvailable() || !testutil::Av1DecoderAvailable()) { |
| GTEST_SKIP() << "AV1 codec unavailable, skip test."; |
| } |
| |
| ImagePtr image = testutil::ReadImage(data_path, "abc.png"); |
| image->transformFlags |= AVIF_TRANSFORM_IROT; |
| image->irot.angle = 1; |
| |
| // Encode. |
| EncoderPtr encoder(avifEncoderCreate()); |
| ASSERT_NE(encoder, nullptr); |
| encoder->quality = AVIF_QUALITY_LOSSLESS; |
| encoder->speed = AVIF_SPEED_FASTEST; |
| testutil::AvifRwData encoded; |
| ASSERT_EQ(avifEncoderWrite(encoder.get(), image.get(), &encoded), |
| AVIF_RESULT_OK); |
| |
| // Decode. |
| ImagePtr decoded(avifImageCreateEmpty()); |
| ASSERT_NE(decoded, nullptr); |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadMemory(decoder.get(), decoded.get(), encoded.data, |
| encoded.size), |
| AVIF_RESULT_OK); |
| // Note that rav1e does not support lossless encoding as of v0.8.0. |
| // Otherwise AreImagesEqual() could be used here. |
| EXPECT_TRUE(testutil::AreImagesSimilar(*image, *decoded)); |
| |
| // Check with existing correct AVIF file. |
| const std::string ref_path = |
| std::string(data_path) + "abc_color_irot_alpha_irot.avif"; |
| ImagePtr ref_decoded(avifImageCreateEmpty()); |
| ASSERT_NE(ref_decoded, nullptr); |
| DecoderPtr ref_decoder(avifDecoderCreate()); |
| ASSERT_NE(ref_decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadFile(ref_decoder.get(), ref_decoded.get(), |
| ref_path.c_str()), |
| AVIF_RESULT_OK); |
| // Note that rav1e does not support lossless encoding as of v0.8.0. |
| // Otherwise AreImagesEqual() could be used here. |
| EXPECT_TRUE(testutil::AreImagesSimilar(*decoded, *ref_decoded)); |
| |
| // The rendering of the images should be compared but that is outside the |
| // scope of libavif. |
| } |
| |
| TEST(TransformTest, AlphaIrotNoIrot) { |
| if (!testutil::Av1DecoderAvailable()) { |
| GTEST_SKIP() << "AV1 codec unavailable, skip test."; |
| } |
| |
| const std::string ref_path = |
| std::string(data_path) + "abc_color_irot_alpha_irot.avif"; |
| ImagePtr ref_decoded(avifImageCreateEmpty()); |
| ASSERT_NE(ref_decoded, nullptr); |
| DecoderPtr ref_decoder(avifDecoderCreate()); |
| ASSERT_NE(ref_decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadFile(ref_decoder.get(), ref_decoded.get(), |
| ref_path.c_str()), |
| AVIF_RESULT_OK); |
| |
| // Read a file missing an 'irot' property associated with the alpha auxiliary |
| // image item. While this should not lead to the same output, libavif still |
| // decodes it the same way because libavif used to encode it without that |
| // association. |
| const std::string path = |
| std::string(data_path) + "abc_color_irot_alpha_NOirot.avif"; |
| ImagePtr decoded(avifImageCreateEmpty()); |
| ASSERT_NE(decoded, nullptr); |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()), |
| AVIF_RESULT_OK); |
| EXPECT_TRUE(testutil::AreImagesEqual(*decoded, *ref_decoded)); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| } // 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(); |
| } |