| // Copyright 2026 Google LLC | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include <array> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <utility> | |
| #include <vector> | |
| #include "avif/avif.h" | |
| #include "avif_fuzztest_helpers.h" | |
| #include "aviftest_helpers.h" | |
| #include "fuzztest/fuzztest.h" | |
| #include "gtest/gtest.h" | |
| namespace avif { | |
| namespace testutil { | |
| namespace { | |
| constexpr size_t kLayerCount = kMaxNumLayers; | |
| constexpr avifFraction kNoScale = {1, 1}; | |
| constexpr size_t kRandomScalingModeCount = kLayerCount - 1; | |
| static_assert(kRandomScalingModeCount == 3); | |
| constexpr std::array<avifFraction, 6> kSupportedScalingModes = { | |
| avifFraction{1, 8}, avifFraction{1, 4}, avifFraction{1, 2}, | |
| avifFraction{3, 5}, avifFraction{3, 4}, avifFraction{4, 5}}; | |
| constexpr avifScalingMode kNoScalingMode = {kNoScale, kNoScale}; | |
| constexpr std::array<avifScalingMode, kLayerCount> kNoScalingModes = { | |
| kNoScalingMode, kNoScalingMode, kNoScalingMode, kNoScalingMode}; | |
| constexpr avifScalingMode MakeScalingMode(avifFraction horizontal, | |
| avifFraction vertical) { | |
| return {horizontal, vertical}; | |
| } | |
| constexpr size_t BinomialCoefficient(size_t n, size_t k) { | |
| if (k > n) { | |
| return 0; | |
| } | |
| if (k > (n - k)) { | |
| k = n - k; | |
| } | |
| size_t coefficient = 1; | |
| for (size_t i = 1; i <= k; ++i) { | |
| coefficient = (coefficient * (n - k + i)) / i; | |
| } | |
| return coefficient; | |
| } | |
| constexpr size_t CombinationCountWithRepetition(size_t value_count, | |
| size_t pick_count) { | |
| return BinomialCoefficient(value_count + pick_count - 1, pick_count); | |
| } | |
| constexpr size_t kScalingModeCombinationCount = CombinationCountWithRepetition( | |
| kSupportedScalingModes.size(), kRandomScalingModeCount); | |
| std::array<avifFraction, kRandomScalingModeCount> GetScalingModeCombination( | |
| size_t combination_index) { | |
| std::array<avifFraction, kRandomScalingModeCount> combination = {}; | |
| size_t remaining_index = combination_index; | |
| size_t next_candidate = 0; | |
| for (size_t slot = 0; slot < combination.size(); ++slot) { | |
| const size_t remaining_slots = combination.size() - slot - 1; | |
| for (size_t candidate = next_candidate; | |
| candidate < kSupportedScalingModes.size(); ++candidate) { | |
| const size_t combinations_with_candidate = CombinationCountWithRepetition( | |
| kSupportedScalingModes.size() - candidate, remaining_slots); | |
| if (remaining_index < combinations_with_candidate) { | |
| combination[slot] = kSupportedScalingModes[candidate]; | |
| next_candidate = candidate; | |
| break; | |
| } | |
| remaining_index -= combinations_with_candidate; | |
| } | |
| } | |
| return combination; | |
| } | |
| std::array<avifScalingMode, kLayerCount> MakeRandomScalingModes( | |
| int horizontal_combination_index, int vertical_combination_index) { | |
| const auto horizontal_combination = | |
| GetScalingModeCombination(horizontal_combination_index); | |
| const auto vertical_combination = | |
| GetScalingModeCombination(vertical_combination_index); | |
| return std::array<avifScalingMode, kLayerCount>{ | |
| MakeScalingMode(horizontal_combination[0], vertical_combination[0]), | |
| MakeScalingMode(horizontal_combination[1], vertical_combination[1]), | |
| MakeScalingMode(horizontal_combination[2], vertical_combination[2]), | |
| kNoScalingMode}; | |
| } | |
| inline auto ArbitraryScalingModes() { | |
| constexpr int kMaxCombinationIndex = | |
| static_cast<int>(kScalingModeCombinationCount - 1); | |
| return fuzztest::Map(MakeRandomScalingModes, | |
| fuzztest::InRange<int>(0, kMaxCombinationIndex), | |
| fuzztest::InRange<int>(0, kMaxCombinationIndex)); | |
| } | |
| void EncodeDecodeLayered( | |
| std::vector<ImagePtr> layers, | |
| const std::array<avifScalingMode, kLayerCount>& scaling_modes, | |
| uint32_t expected_width, uint32_t expected_height, bool set_last_layer_size, | |
| EncoderPtr encoder, DecoderPtr decoder) { | |
| ASSERT_EQ(layers.size(), kLayerCount); | |
| ASSERT_NE(encoder, nullptr); | |
| ASSERT_NE(decoder, nullptr); | |
| if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) == | |
| nullptr) { | |
| return; | |
| } | |
| if (avifLibYUVVersion() == 0) { | |
| return; | |
| } | |
| const avifImage* const reference = layers.front().get(); | |
| ASSERT_NE(reference, nullptr); | |
| encoder->codecChoice = AVIF_CODEC_CHOICE_AOM; | |
| encoder->extraLayerCount = static_cast<uint32_t>(layers.size() - 1); | |
| if (set_last_layer_size) { | |
| encoder->width = expected_width; | |
| encoder->height = expected_height; | |
| } | |
| for (size_t i = 0; i < layers.size(); ++i) { | |
| ASSERT_NE(layers[i].get(), nullptr); | |
| encoder->scalingMode = scaling_modes[i]; | |
| const avifResult result = avifEncoderAddImage( | |
| encoder.get(), layers[i].get(), | |
| /*durationInTimescales=*/1, AVIF_ADD_IMAGE_FLAG_NONE); | |
| ASSERT_EQ(result, AVIF_RESULT_OK) | |
| << " layer " << i << ": " << avifResultToString(result) << ": " | |
| << encoder->diag.error; | |
| } | |
| AvifRwData encoded_data; | |
| avifResult result = avifEncoderFinish(encoder.get(), &encoded_data); | |
| ASSERT_EQ(result, AVIF_RESULT_OK) | |
| << avifResultToString(result) << ": " << encoder->diag.error; | |
| result = avifDecoderSetIOMemory(decoder.get(), encoded_data.data, | |
| encoded_data.size); | |
| ASSERT_EQ(result, AVIF_RESULT_OK) | |
| << avifResultToString(result) << ": " << decoder->diag.error; | |
| result = avifDecoderParse(decoder.get()); | |
| ASSERT_EQ(result, AVIF_RESULT_OK) | |
| << avifResultToString(result) << ": " << decoder->diag.error; | |
| const int num_decodes = | |
| decoder->allowProgressive ? static_cast<int>(layers.size()) : 1; | |
| if (decoder->allowProgressive) { | |
| EXPECT_EQ(decoder->progressiveState, AVIF_PROGRESSIVE_STATE_ACTIVE); | |
| ASSERT_EQ(decoder->imageCount, static_cast<int>(layers.size())); | |
| } else { | |
| EXPECT_EQ(decoder->progressiveState, AVIF_PROGRESSIVE_STATE_AVAILABLE); | |
| ASSERT_EQ(decoder->imageCount, 1); | |
| } | |
| for (int i = 0; i < num_decodes; ++i) { | |
| result = avifDecoderNextImage(decoder.get()); | |
| ASSERT_EQ(result, AVIF_RESULT_OK) | |
| << " layer " << i << ": " << avifResultToString(result) << ": " | |
| << decoder->diag.error; | |
| EXPECT_EQ(decoder->image->width, expected_width); | |
| EXPECT_EQ(decoder->image->height, expected_height); | |
| EXPECT_EQ(decoder->image->depth, reference->depth); | |
| EXPECT_EQ(decoder->image->yuvFormat, reference->yuvFormat); | |
| } | |
| result = avifDecoderNextImage(decoder.get()); | |
| ASSERT_EQ(result, AVIF_RESULT_NO_IMAGES_REMAINING) | |
| << avifResultToString(result) << ": " << decoder->diag.error; | |
| } | |
| void EncodeDecodeDimensionChange( | |
| std::vector<ImagePtr> layers, | |
| const std::array<avifScalingMode, kLayerCount>& scaling_modes, | |
| EncoderPtr encoder, DecoderPtr decoder) { | |
| ASSERT_EQ(layers.size(), kLayerCount); | |
| ASSERT_NE(layers.back().get(), nullptr); | |
| const uint32_t expected_width = layers.back()->width; | |
| const uint32_t expected_height = layers.back()->height; | |
| EncodeDecodeLayered(std::move(layers), scaling_modes, | |
| /*expected_width=*/expected_width, | |
| /*expected_height=*/expected_height, | |
| /*set_last_layer_size=*/false, std::move(encoder), | |
| std::move(decoder)); | |
| } | |
| void EncodeDecodeDimensionChangeExternal(std::vector<ImagePtr> layers, | |
| EncoderPtr encoder, | |
| DecoderPtr decoder) { | |
| ASSERT_EQ(layers.size(), kLayerCount); | |
| ASSERT_NE(layers.back().get(), nullptr); | |
| const uint32_t expected_width = layers.back()->width; | |
| const uint32_t expected_height = layers.back()->height; | |
| EncodeDecodeLayered(std::move(layers), kNoScalingModes, | |
| /*expected_width=*/expected_width, | |
| /*expected_height=*/expected_height, | |
| /*set_last_layer_size=*/true, std::move(encoder), | |
| std::move(decoder)); | |
| } | |
| FUZZ_TEST(LayeredEncodeDecodeAvifFuzzTest, EncodeDecodeDimensionChange) | |
| .WithDomains(ArbitraryAvifLayered(), ArbitraryScalingModes(), | |
| ArbitraryAvifEncoder(), ArbitraryAvifDecoder()); | |
| FUZZ_TEST(LayeredEncodeDecodeAvifFuzzTest, EncodeDecodeDimensionChangeExternal) | |
| .WithDomains(ArbitraryAvifLayeredRandDim(), ArbitraryAvifEncoder(), | |
| ArbitraryAvifDecoder()); | |
| } // namespace | |
| } // namespace testutil | |
| } // namespace avif |