| // 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(); | |
| } |