| // Copyright 2022 Google LLC | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include "avif_fuzztest_helpers.h" | |
| #include <algorithm> | |
| #include <array> | |
| #include <cassert> | |
| #include <cstdint> | |
| #include <cstdlib> | |
| #include <iostream> | |
| #include <utility> | |
| #include <vector> | |
| #include "avif/avif.h" | |
| #include "avifutil.h" | |
| #include "fuzztest/fuzztest.h" | |
| #include "gtest/gtest.h" | |
| namespace avif { | |
| namespace testutil { | |
| namespace { | |
| //------------------------------------------------------------------------------ | |
| // The number of seeds for kMaxNumLayers - 1 pairs of random width and height. | |
| constexpr size_t kNumLayeredRandDimSeeds = 2 * (kMaxNumLayers - 1); | |
| // Generates kMaxNumLayers pairs of random width and height. The first | |
| // kMaxNumLayers - 1 pairs of width and height are less than last_layer_width | |
| // and last_layer_height, respectively. The last pair is the given | |
| // last_layer_width and last_layer_height. | |
| template <typename Sample> | |
| std::array<std::pair<size_t, size_t>, kMaxNumLayers> GetAvifLayeredRandDims( | |
| size_t last_layer_width, size_t last_layer_height, | |
| const std::vector<Sample>& seeds) { | |
| assert(last_layer_width > 1); | |
| assert(last_layer_height > 1); | |
| std::array<std::pair<size_t, size_t>, kMaxNumLayers> sizes = {}; | |
| for (size_t i = 0; i < kMaxNumLayers - 1; ++i) { | |
| const size_t width = | |
| 1 + (static_cast<size_t>(seeds[2 * i]) % (last_layer_width - 1)); | |
| const size_t height = | |
| 1 + (static_cast<size_t>(seeds[2 * i + 1]) % (last_layer_height - 1)); | |
| assert(width < last_layer_width); | |
| assert(height < last_layer_height); | |
| sizes[i] = {width, height}; | |
| } | |
| sizes[kMaxNumLayers - 1] = {last_layer_width, last_layer_height}; | |
| return sizes; | |
| } | |
| ImagePtr CreateAvifImage(size_t width, size_t height, int depth, | |
| avifPixelFormat pixel_format, bool has_alpha, | |
| const uint8_t* samples) { | |
| ImagePtr image(avifImageCreate(static_cast<uint32_t>(width), | |
| static_cast<uint32_t>(height), depth, | |
| pixel_format)); | |
| if (image.get() == nullptr) { | |
| return image; | |
| } | |
| if (avifImageAllocatePlanes(image.get(), | |
| has_alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV) != | |
| AVIF_RESULT_OK) { | |
| return nullptr; | |
| } | |
| for (avifChannelIndex c : | |
| {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { | |
| const size_t plane_height = avifImagePlaneHeight(image.get(), c); | |
| uint8_t* row = avifImagePlane(image.get(), c); | |
| const uint32_t row_bytes = avifImagePlaneRowBytes(image.get(), c); | |
| assert(row_bytes == avifImagePlaneWidth(image.get(), c) * | |
| (avifImageUsesU16(image.get()) ? 2 : 1)); | |
| for (size_t y = 0; y < plane_height; ++y) { | |
| std::copy(samples, samples + row_bytes, row); | |
| row += row_bytes; | |
| samples += row_bytes; | |
| } | |
| } | |
| return image; | |
| } | |
| } // namespace | |
| ImagePtr CreateAvifImage8b(size_t width, size_t height, | |
| avifPixelFormat pixel_format, bool has_alpha, | |
| const std::vector<uint8_t>& samples) { | |
| return CreateAvifImage(width, height, 8, pixel_format, has_alpha, | |
| samples.data()); | |
| } | |
| ImagePtr CreateAvifImage16b(size_t width, size_t height, int depth, | |
| avifPixelFormat pixel_format, bool has_alpha, | |
| const std::vector<uint16_t>& samples) { | |
| return CreateAvifImage(width, height, depth, pixel_format, has_alpha, | |
| reinterpret_cast<const uint8_t*>(samples.data())); | |
| } | |
| std::vector<ImagePtr> CreateAvifAnim8b(size_t num_frames, size_t width, | |
| size_t height, | |
| avifPixelFormat pixel_format, | |
| bool has_alpha, | |
| const std::vector<uint8_t>& samples) { | |
| std::vector<ImagePtr> frames; | |
| frames.reserve(num_frames); | |
| const uint8_t* frame_samples = samples.data(); | |
| for (size_t i = 0; i < num_frames; ++i) { | |
| frames.push_back(CreateAvifImage(width, height, 8, pixel_format, has_alpha, | |
| frame_samples)); | |
| frame_samples += | |
| GetNumSamples(/*num_frames=*/1, width, height, pixel_format, has_alpha); | |
| } | |
| return frames; | |
| } | |
| std::vector<ImagePtr> CreateAvifAnim16b(size_t num_frames, size_t width, | |
| size_t height, int depth, | |
| avifPixelFormat pixel_format, | |
| bool has_alpha, | |
| const std::vector<uint16_t>& samples) { | |
| std::vector<ImagePtr> frames; | |
| frames.reserve(num_frames); | |
| const uint16_t* frame_samples = samples.data(); | |
| for (size_t i = 0; i < num_frames; ++i) { | |
| frames.push_back( | |
| CreateAvifImage(width, height, depth, pixel_format, has_alpha, | |
| reinterpret_cast<const uint8_t*>(frame_samples))); | |
| frame_samples += | |
| GetNumSamples(/*num_frames=*/1, width, height, pixel_format, has_alpha); | |
| } | |
| return frames; | |
| } | |
| std::vector<ImagePtr> CreateAvifLayered8b(size_t width, size_t height, | |
| avifPixelFormat pixel_format, | |
| bool has_alpha, | |
| const std::vector<uint8_t>& samples) { | |
| return CreateAvifAnim8b(kMaxNumLayers, width, height, pixel_format, has_alpha, | |
| samples); | |
| } | |
| std::vector<ImagePtr> CreateAvifLayered16b( | |
| size_t width, size_t height, int depth, avifPixelFormat pixel_format, | |
| bool has_alpha, const std::vector<uint16_t>& samples) { | |
| return CreateAvifAnim16b(kMaxNumLayers, width, height, depth, pixel_format, | |
| has_alpha, samples); | |
| } | |
| size_t GetNumSamplesLayeredRandDim(size_t last_layer_width, | |
| size_t last_layer_height, | |
| avifPixelFormat pixel_format, | |
| bool has_alpha) { | |
| return kNumLayeredRandDimSeeds + | |
| GetNumSamples(kMaxNumLayers, last_layer_width, last_layer_height, | |
| pixel_format, has_alpha); | |
| } | |
| std::vector<ImagePtr> CreateAvifLayeredRandDim8b( | |
| size_t last_layer_width, size_t last_layer_height, | |
| avifPixelFormat pixel_format, bool has_alpha, | |
| const std::vector<uint8_t>& samples) { | |
| assert(samples.size() >= kNumLayeredRandDimSeeds); | |
| const auto dims = | |
| GetAvifLayeredRandDims(last_layer_width, last_layer_height, samples); | |
| std::vector<ImagePtr> layers; | |
| layers.reserve(kMaxNumLayers); | |
| size_t offset = kNumLayeredRandDimSeeds; | |
| for (const auto& [width, height] : dims) { | |
| const size_t num_samples = GetNumSamples( | |
| /*num_frames=*/1, width, height, pixel_format, has_alpha); | |
| layers.push_back(CreateAvifImage8b( | |
| width, height, pixel_format, has_alpha, | |
| std::vector<uint8_t>(samples.begin() + offset, | |
| samples.begin() + offset + num_samples))); | |
| offset += num_samples; | |
| } | |
| return layers; | |
| } | |
| std::vector<ImagePtr> CreateAvifLayeredRandDim16b( | |
| size_t last_layer_width, size_t last_layer_height, int depth, | |
| avifPixelFormat pixel_format, bool has_alpha, | |
| const std::vector<uint16_t>& samples) { | |
| assert(samples.size() >= kNumLayeredRandDimSeeds); | |
| const auto dims = | |
| GetAvifLayeredRandDims(last_layer_width, last_layer_height, samples); | |
| std::vector<ImagePtr> layers; | |
| layers.reserve(kMaxNumLayers); | |
| size_t offset = kNumLayeredRandDimSeeds; | |
| for (const auto& [width, height] : dims) { | |
| const size_t num_samples = GetNumSamples( | |
| /*num_frames=*/1, width, height, pixel_format, has_alpha); | |
| layers.push_back(CreateAvifImage16b( | |
| width, height, depth, pixel_format, has_alpha, | |
| std::vector<uint16_t>(samples.begin() + offset, | |
| samples.begin() + offset + num_samples))); | |
| offset += num_samples; | |
| } | |
| return layers; | |
| } | |
| EncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads, | |
| int quality, int quality_alpha, int tile_rows_log2, | |
| int tile_cols_log2, int speed) { | |
| EncoderPtr encoder(avifEncoderCreate()); | |
| if (encoder.get() == nullptr) { | |
| return encoder; | |
| } | |
| encoder->codecChoice = codec_choice; | |
| encoder->maxThreads = max_threads; | |
| encoder->quality = quality; | |
| encoder->qualityAlpha = quality_alpha; | |
| encoder->tileRowsLog2 = tile_rows_log2; | |
| encoder->tileColsLog2 = tile_cols_log2; | |
| encoder->speed = speed; | |
| return encoder; | |
| } | |
| DecoderPtr CreateAvifDecoder(avifCodecChoice codec_choice, int max_threads, | |
| avifDecoderSource requested_source, | |
| bool allow_progressive, bool allow_incremental, | |
| bool ignore_exif, bool ignore_xmp, | |
| uint32_t image_size_limit, | |
| uint32_t image_dimension_limit, | |
| uint32_t image_count_limit, | |
| avifStrictFlags strict_flags) { | |
| DecoderPtr decoder(avifDecoderCreate()); | |
| if (decoder.get() == nullptr) { | |
| return decoder; | |
| } | |
| decoder->codecChoice = codec_choice; | |
| decoder->maxThreads = max_threads; | |
| decoder->requestedSource = requested_source; | |
| decoder->allowProgressive = allow_progressive; | |
| decoder->allowIncremental = allow_incremental; | |
| decoder->ignoreExif = ignore_exif; | |
| decoder->ignoreXMP = ignore_xmp; | |
| decoder->imageSizeLimit = image_size_limit; | |
| decoder->imageDimensionLimit = image_dimension_limit; | |
| decoder->imageCountLimit = image_count_limit; | |
| decoder->strictFlags = strict_flags; | |
| return decoder; | |
| } | |
| ImagePtr AvifImageToUniquePtr(avifImage* image) { return ImagePtr(image); } | |
| DecoderPtr AddGainMapOptionsToDecoder( | |
| DecoderPtr decoder, avifImageContentTypeFlags image_content_to_decode) { | |
| decoder->imageContentToDecode = image_content_to_decode; | |
| return decoder; | |
| } | |
| ImagePtr AddGainMapToImage( | |
| ImagePtr image, ImagePtr gain_map, | |
| const std::pair<avifSignedFraction, avifSignedFraction>& gain_map_min_max0, | |
| const std::pair<avifSignedFraction, avifSignedFraction>& gain_map_min_max1, | |
| const std::pair<avifSignedFraction, avifSignedFraction>& gain_map_min_max2, | |
| uint32_t gain_map_gamma_n0, uint32_t gain_map_gamma_n1, | |
| uint32_t gain_map_gamma_n2, uint32_t gain_map_gamma_d0, | |
| uint32_t gain_map_gamma_d1, uint32_t gain_map_gamma_d2, | |
| int32_t base_offset_n0, int32_t base_offset_n1, int32_t base_offset_n2, | |
| uint32_t base_offset_d0, uint32_t base_offset_d1, uint32_t base_offset_d2, | |
| int32_t alternate_offset_n0, int32_t alternate_offset_n1, | |
| int32_t alternate_offset_n2, uint32_t alternate_offset_d0, | |
| uint32_t alternate_offset_d1, uint32_t alternate_offset_d2, | |
| uint32_t base_hdr_headroom_n, uint32_t base_hdr_headroom_d, | |
| uint32_t alternate_hdr_headroom_n, uint32_t alternate_hdr_headroom_d, | |
| bool use_base_color_space) { | |
| image->gainMap = avifGainMapCreate(); | |
| image->gainMap->image = gain_map.release(); | |
| image->gainMap->gainMapMin[0] = gain_map_min_max0.first; | |
| image->gainMap->gainMapMin[1] = gain_map_min_max1.first; | |
| image->gainMap->gainMapMin[2] = gain_map_min_max2.first; | |
| image->gainMap->gainMapMax[0] = gain_map_min_max0.second; | |
| image->gainMap->gainMapMax[1] = gain_map_min_max1.second; | |
| image->gainMap->gainMapMax[2] = gain_map_min_max2.second; | |
| image->gainMap->gainMapGamma[0] = {gain_map_gamma_n0, gain_map_gamma_d0}; | |
| image->gainMap->gainMapGamma[1] = {gain_map_gamma_n1, gain_map_gamma_d1}; | |
| image->gainMap->gainMapGamma[2] = {gain_map_gamma_n2, gain_map_gamma_d2}; | |
| image->gainMap->baseOffset[0] = {base_offset_n0, base_offset_d0}; | |
| image->gainMap->baseOffset[1] = {base_offset_n1, base_offset_d1}; | |
| image->gainMap->baseOffset[2] = {base_offset_n2, base_offset_d2}; | |
| image->gainMap->alternateOffset[0] = {alternate_offset_n0, | |
| alternate_offset_d0}; | |
| image->gainMap->alternateOffset[1] = {alternate_offset_n1, | |
| alternate_offset_d1}; | |
| image->gainMap->alternateOffset[2] = {alternate_offset_n2, | |
| alternate_offset_d2}; | |
| image->gainMap->baseHdrHeadroom = {base_hdr_headroom_n, base_hdr_headroom_d}; | |
| image->gainMap->alternateHdrHeadroom = {alternate_hdr_headroom_n, | |
| alternate_hdr_headroom_d}; | |
| image->gainMap->useBaseColorSpace = use_base_color_space; | |
| return image; | |
| } | |
| //------------------------------------------------------------------------------ | |
| size_t GetNumSamples(size_t num_frames, size_t width, size_t height, | |
| avifPixelFormat pixel_format, bool has_alpha) { | |
| const size_t num_luma_samples = width * height; | |
| avifPixelFormatInfo pixel_format_info; | |
| avifGetPixelFormatInfo(pixel_format, &pixel_format_info); | |
| size_t num_chroma_samples = 0; | |
| if (!pixel_format_info.monochrome) { | |
| num_chroma_samples = 2 * | |
| ((width + pixel_format_info.chromaShiftX) >> | |
| pixel_format_info.chromaShiftX) * | |
| ((height + pixel_format_info.chromaShiftY) >> | |
| pixel_format_info.chromaShiftY); | |
| } | |
| size_t num_alpha_samples = 0; | |
| if (has_alpha) { | |
| num_alpha_samples = width * height; | |
| } | |
| return num_frames * | |
| (num_luma_samples + num_chroma_samples + num_alpha_samples); | |
| } | |
| //------------------------------------------------------------------------------ | |
| std::vector<std::string> GetSeedDataDirs() { | |
| const char* var = std::getenv("TEST_DATA_DIRS"); | |
| std::vector<std::string> res; | |
| if (var == nullptr || *var == 0) return res; | |
| const char* var_start = var; | |
| while (true) { | |
| if (*var == 0 || *var == ';') { | |
| res.push_back(std::string(var_start, var - var_start)); | |
| if (*var == 0) break; | |
| var_start = var + 1; | |
| } | |
| ++var; | |
| } | |
| return res; | |
| } | |
| std::vector<std::string> GetTestImagesContents( | |
| size_t max_file_size, const std::vector<avifAppFileFormat>& image_formats) { | |
| // Use an environment variable to get the test data directory because | |
| // fuzztest seeds are created before the main() function is called, so the | |
| // test has no chance to parse command line arguments. | |
| const std::vector<std::string> test_data_dirs = GetSeedDataDirs(); | |
| if (test_data_dirs.empty()) { | |
| // Only a warning because this can happen when running the binary with | |
| // --list_fuzz_tests (such as with gtest_discover_tests() in cmake). | |
| std::cerr << "WARNING: TEST_DATA_DIRS env variable not set, unable to read " | |
| "seed files\n"; | |
| return {}; | |
| } | |
| std::vector<std::string> seeds; | |
| for (const std::string& test_data_dir : test_data_dirs) { | |
| std::cout << "Reading seeds from " << test_data_dir | |
| << " (non recursively)\n"; | |
| auto tuple_vector = fuzztest::ReadFilesFromDirectory(test_data_dir); | |
| seeds.reserve(tuple_vector.size()); | |
| for (auto& [file_content] : tuple_vector) { | |
| if (file_content.size() > max_file_size) continue; | |
| if (!image_formats.empty()) { | |
| const avifAppFileFormat format = avifGuessBufferFileFormat( | |
| reinterpret_cast<const uint8_t*>(file_content.data()), | |
| file_content.size()); | |
| if (std::find(image_formats.begin(), image_formats.end(), format) == | |
| image_formats.end()) { | |
| continue; | |
| } | |
| } | |
| seeds.push_back(std::move(file_content)); | |
| } | |
| } | |
| if (seeds.empty()) { | |
| std::cerr << "ERROR: no files found that match the given file size and " | |
| "format criteria\n"; | |
| std::abort(); | |
| } | |
| std::cout << "Returning " << seeds.size() << " seed images\n"; | |
| return seeds; | |
| } | |
| //------------------------------------------------------------------------------ | |
| } // namespace testutil | |
| } // namespace avif |