| // 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 |