| // Copyright 2023 Google LLC | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include <algorithm> | |
| #include <cstring> | |
| #include <tuple> | |
| #include "aviftest_helpers.h" | |
| #include "gtest/gtest.h" | |
| namespace avif { | |
| namespace { | |
| enum class InputType { kStillImage, kGrid, kAnimation }; | |
| // Generates a valid AVIF stream with the video range flag set to the same value | |
| // in the "colr" box and in the AV1 OBU payload (in the "mdat" box). | |
| avifResult GenerateEncodedData(avifRange range, InputType input_type, | |
| testutil::AvifRwData* encoded) { | |
| ImagePtr image = | |
| testutil::CreateImage(/*width=*/64, /*height=*/64, /*depth=*/8, | |
| AVIF_PIXEL_FORMAT_YUV420, AVIF_PLANES_ALL, range); | |
| AVIF_CHECKERR(image != nullptr, AVIF_RESULT_OUT_OF_MEMORY); | |
| testutil::FillImageGradient(image.get()); // Pixel values do not matter. | |
| EncoderPtr encoder(avifEncoderCreate()); | |
| AVIF_CHECKERR(encoder != nullptr, AVIF_RESULT_OUT_OF_MEMORY); | |
| if (input_type == InputType::kStillImage) { | |
| AVIF_CHECKRES(avifEncoderWrite(encoder.get(), image.get(), encoded)); | |
| } else if (input_type == InputType::kGrid) { | |
| const avifImage* cellImages[2] = {image.get(), image.get()}; | |
| AVIF_CHECKRES(avifEncoderAddImageGrid(encoder.get(), /*gridCols=*/2, | |
| /*gridRows=*/1, cellImages, | |
| AVIF_ADD_IMAGE_FLAG_SINGLE)); | |
| AVIF_CHECKRES(avifEncoderFinish(encoder.get(), encoded)); | |
| } else { | |
| assert(input_type == InputType::kAnimation); | |
| AVIF_CHECKRES(avifEncoderAddImage(encoder.get(), image.get(), | |
| /*durationInTimescales=*/1, | |
| AVIF_ADD_IMAGE_FLAG_NONE)); | |
| AVIF_CHECKRES(avifEncoderAddImage(encoder.get(), image.get(), | |
| /*durationInTimescales=*/1, | |
| AVIF_ADD_IMAGE_FLAG_NONE)); | |
| AVIF_CHECKRES(avifEncoderFinish(encoder.get(), encoded)); | |
| } | |
| return AVIF_RESULT_OK; | |
| } | |
| class RangeTest | |
| : public testing::TestWithParam<std::tuple<avifRange, InputType>> {}; | |
| TEST_P(RangeTest, DifferentVideoRangeInColrAndMdat) { | |
| const avifRange obu_range = std::get<0>(GetParam()); | |
| const InputType input_type = std::get<1>(GetParam()); | |
| testutil::AvifRwData encoded; | |
| ASSERT_EQ(GenerateEncodedData(obu_range, input_type, &encoded), | |
| AVIF_RESULT_OK); | |
| // Set full_range_flag in the "colr" box to a different value. | |
| // This creates an invalid bitstream according to AV1-ISOBMFF v1.2.0 but | |
| // libavif still allows it. | |
| const avifRange colr_range = | |
| (obu_range == AVIF_RANGE_LIMITED) ? AVIF_RANGE_FULL : AVIF_RANGE_LIMITED; | |
| const uint8_t kColrBoxTag[] = "colr"; | |
| uint8_t* colr_box = std::search(encoded.data, encoded.data + encoded.size, | |
| kColrBoxTag, kColrBoxTag + 4); | |
| do { | |
| ASSERT_GT(colr_box, encoded.data + 4); | |
| ASSERT_LT(colr_box, encoded.data + encoded.size); | |
| const uint32_t colr_box_size = (colr_box[-4] << 24) | (colr_box[-3] << 16) | | |
| (colr_box[-2] << 8) | colr_box[-1]; | |
| ASSERT_EQ(colr_box_size, 19u); | |
| ASSERT_LT(colr_box + colr_box_size - 4, encoded.data + encoded.size); | |
| ASSERT_TRUE(std::equal(colr_box + 4, colr_box + 8, | |
| reinterpret_cast<const uint8_t*>("nclx"))); | |
| // full_range_flag(1bit)=colr_range, reserved(7bits)=0 | |
| colr_box[colr_box_size - 5] = static_cast<uint8_t>(colr_range << 7); | |
| colr_box = std::search(colr_box + 4, encoded.data + encoded.size, | |
| kColrBoxTag, kColrBoxTag + 4); | |
| } while (colr_box != encoded.data + encoded.size); | |
| // Section 12.1.5.1 of ISO 14496-12 (ISOBMFF) says: | |
| // If colour information is supplied in both this [colr] box, and also in | |
| // the video bitstream, this box takes precedence, and over-rides the | |
| // information in the bitstream. | |
| 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); | |
| ASSERT_EQ(decoded->yuvRange, colr_range); | |
| } | |
| TEST_P(RangeTest, MissingColr) { | |
| const avifRange obu_range = std::get<0>(GetParam()); | |
| const InputType input_type = std::get<1>(GetParam()); | |
| testutil::AvifRwData encoded; | |
| ASSERT_EQ(GenerateEncodedData(obu_range, input_type, &encoded), | |
| AVIF_RESULT_OK); | |
| // Remove the "colr" box (by replacing it with a placeholder). | |
| // This creates an invalid bitstream according to AV1-ISOBMFF v1.2.0 but | |
| // libavif still allows it. | |
| const uint8_t kColrBoxTag[] = "colr"; | |
| uint8_t* colr_box = std::search(encoded.data, encoded.data + encoded.size, | |
| kColrBoxTag, kColrBoxTag + 4); | |
| do { | |
| ASSERT_LT(colr_box + 4, encoded.data + encoded.size); | |
| std::memcpy(colr_box, "free", 4); | |
| colr_box = std::search(colr_box + 4, encoded.data + encoded.size, | |
| kColrBoxTag, kColrBoxTag + 4); | |
| } while (colr_box != encoded.data + encoded.size); | |
| // Make sure the AV1 OBU range is kept. | |
| 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); | |
| ASSERT_EQ(decoded->yuvRange, obu_range); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| All, RangeTest, | |
| testing::Combine(testing::Values(AVIF_RANGE_LIMITED, AVIF_RANGE_FULL), | |
| testing::Values(InputType::kStillImage, InputType::kGrid, | |
| InputType::kAnimation))); | |
| } // namespace | |
| } // namespace avif |