| // Copyright 2022 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include <fstream> |
| #include <iostream> |
| #include <string> |
| |
| #include "avif/avif.h" |
| #include "gtest/gtest.h" |
| #include "testutil.h" |
| |
| using testing::Bool; |
| using testing::Combine; |
| using testing::Values; |
| |
| namespace avif { |
| namespace { |
| |
| // Used to pass the data folder path to the GoogleTest suites. |
| const char* data_path = nullptr; |
| |
| // Verifies that the first (top) row_count rows of image1 and image2 are |
| // identical. |
| void ComparePartialYuva(const avifImage& image1, const avifImage& image2, |
| uint32_t row_count) { |
| if (row_count == 0) { |
| return; |
| } |
| ASSERT_EQ(image1.width, image2.width); |
| ASSERT_GE(image1.height, row_count); |
| ASSERT_GE(image2.height, row_count); |
| ASSERT_EQ(image1.depth, image2.depth); |
| ASSERT_EQ(image1.yuvFormat, image2.yuvFormat); |
| ASSERT_EQ(image1.yuvRange, image2.yuvRange); |
| |
| avifPixelFormatInfo info; |
| avifGetPixelFormatInfo(image1.yuvFormat, &info); |
| const uint32_t uv_height = |
| info.monochrome ? 0 |
| : ((row_count + info.chromaShiftY) >> info.chromaShiftY); |
| const size_t pixel_byte_count = |
| (image1.depth > 8) ? sizeof(uint16_t) : sizeof(uint8_t); |
| |
| if (image1.alphaPlane) { |
| ASSERT_NE(image2.alphaPlane, nullptr); |
| ASSERT_EQ(image1.alphaPremultiplied, image2.alphaPremultiplied); |
| } |
| |
| const int last_plane = image1.alphaPlane ? AVIF_CHAN_A : AVIF_CHAN_V; |
| for (int plane = AVIF_CHAN_Y; plane <= last_plane; ++plane) { |
| const size_t width_byte_count = |
| avifImagePlaneWidth(&image1, plane) * pixel_byte_count; |
| const uint32_t height = |
| (plane == AVIF_CHAN_Y || plane == AVIF_CHAN_A) ? row_count : uv_height; |
| const uint8_t* row1 = avifImagePlane(&image1, plane); |
| ASSERT_NE(row1, nullptr); |
| const uint8_t* row2 = avifImagePlane(&image2, plane); |
| ASSERT_NE(row2, nullptr); |
| const uint32_t row1_bytes = avifImagePlaneRowBytes(&image1, plane); |
| const uint32_t row2_bytes = avifImagePlaneRowBytes(&image2, plane); |
| for (uint32_t y = 0; y < height; ++y) { |
| ASSERT_EQ(memcmp(row1, row2, width_byte_count), 0); |
| row1 += row1_bytes; |
| row2 += row2_bytes; |
| } |
| } |
| } |
| |
| // Returns the expected number of decoded rows when available_byte_count out of |
| // byte_count were given to the decoder, for an image of height rows, split into |
| // cells of cell_height rows. |
| uint32_t GetMinDecodedRowCount(uint32_t height, uint32_t cell_height, |
| bool has_alpha, size_t available_byte_count, |
| size_t byte_count, |
| bool enable_fine_incremental_check) { |
| // The whole image should be available when the full input is. |
| if (available_byte_count >= byte_count) { |
| return height; |
| } |
| // All but one cell should be decoded if at most 10 bytes are missing. |
| if ((available_byte_count + 10) >= byte_count) { |
| return height - cell_height; |
| } |
| |
| // The tests below can be hard to tune for any kind of input, especially |
| // fuzzed grids. Early exit in that case. |
| if (!enable_fine_incremental_check) return 0; |
| |
| // Subtract the header because decoding it does not output any pixel. |
| // Most AVIF headers are below 500 bytes. |
| if (available_byte_count <= 500) { |
| return 0; |
| } |
| available_byte_count -= 500; |
| byte_count -= 500; |
| // Alpha, if any, is assumed to be located before the other planes and to |
| // represent at most 50% of the payload. |
| if (has_alpha) { |
| if (available_byte_count <= (byte_count / 2)) { |
| return 0; |
| } |
| available_byte_count -= byte_count / 2; |
| byte_count -= byte_count / 2; |
| } |
| // Linearly map the input availability ratio to the decoded row ratio. |
| const uint32_t min_decoded_cell_row_count = static_cast<uint32_t>( |
| (height / cell_height) * available_byte_count / byte_count); |
| const uint32_t min_decoded_px_row_count = |
| min_decoded_cell_row_count * cell_height; |
| // One cell is the incremental decoding granularity. |
| // It is unlikely that bytes are evenly distributed among cells. Offset two of |
| // them. |
| if (min_decoded_px_row_count <= (2 * cell_height)) { |
| return 0; |
| } |
| return min_decoded_px_row_count - 2 * cell_height; |
| } |
| |
| struct PartialData { |
| avifROData available; |
| size_t full_size; |
| |
| // Only used as nonpersistent input. |
| std::unique_ptr<uint8_t[]> nonpersistent_bytes; |
| size_t num_nonpersistent_bytes; |
| }; |
| |
| // Implementation of avifIOReadFunc simulating a stream from an array. See |
| // avifIOReadFunc documentation. io->data is expected to point to PartialData. |
| avifResult PartialRead(struct avifIO* io, uint32_t read_flags, |
| uint64_t offset64, size_t size, avifROData* out) { |
| PartialData* data = reinterpret_cast<PartialData*>(io->data); |
| if ((read_flags != 0) || !data || (data->full_size < offset64)) { |
| return AVIF_RESULT_IO_ERROR; |
| } |
| const size_t offset = static_cast<size_t>(offset64); |
| // Use |offset| instead of |offset64| from this point on. |
| if (size > (data->full_size - offset)) { |
| size = data->full_size - offset; |
| } |
| if (data->available.size < (offset + size)) { |
| return AVIF_RESULT_WAITING_ON_IO; |
| } |
| if (io->persistent) { |
| out->data = data->available.data + offset; |
| } else { |
| // Dedicated buffer containing just the available bytes and nothing more. |
| std::unique_ptr<uint8_t[]> bytes(new uint8_t[size]); |
| std::copy(data->available.data + offset, |
| data->available.data + offset + size, bytes.get()); |
| out->data = bytes.get(); |
| // Flip the previously returned bytes to make sure the values changed. |
| for (size_t i = 0; i < data->num_nonpersistent_bytes; ++i) { |
| data->nonpersistent_bytes[i] = ~data->nonpersistent_bytes[i]; |
| } |
| // Free the memory to invalidate the old pointer. Only do that after |
| // allocating the new bytes to make sure to have a different pointer. |
| data->nonpersistent_bytes = std::move(bytes); |
| data->num_nonpersistent_bytes = size; |
| } |
| out->size = size; |
| return AVIF_RESULT_OK; |
| } |
| |
| avifResult DecodeIncrementally(const avifRWData& encoded_avif, |
| avifDecoder* decoder, bool is_persistent, |
| bool give_size_hint, bool use_nth_image_api, |
| const avifImage& reference, uint32_t cell_height, |
| bool enable_fine_incremental_check, |
| bool expect_whole_file_read) { |
| // AVIF cells are at least 64 pixels tall. |
| if (cell_height != reference.height) { |
| AVIF_CHECKERR(cell_height >= 64u, AVIF_RESULT_INVALID_ARGUMENT); |
| } |
| |
| // Emulate a byte-by-byte stream. |
| PartialData data = { |
| /*available=*/{encoded_avif.data, 0}, /*fullSize=*/encoded_avif.size, |
| /*nonpersistent_bytes=*/nullptr, /*num_nonpersistent_bytes=*/0}; |
| avifIO io = { |
| /*destroy=*/nullptr, PartialRead, |
| /*write=*/nullptr, give_size_hint ? encoded_avif.size : 0, |
| is_persistent, &data}; |
| avifDecoderSetIO(decoder, &io); |
| decoder->allowIncremental = AVIF_TRUE; |
| const size_t step = std::max<size_t>(1, data.full_size / 10000); |
| |
| // Parsing is not incremental. |
| avifResult parse_result = avifDecoderParse(decoder); |
| while (parse_result == AVIF_RESULT_WAITING_ON_IO) { |
| if (data.available.size >= data.full_size) { |
| std::cerr << "avifDecoderParse() returned WAITING_ON_IO instead of OK" |
| << std::endl; |
| return AVIF_RESULT_TRUNCATED_DATA; |
| } |
| data.available.size = std::min(data.available.size + step, data.full_size); |
| parse_result = avifDecoderParse(decoder); |
| } |
| EXPECT_EQ(parse_result, AVIF_RESULT_OK); |
| |
| // Decoding is incremental. |
| uint32_t previously_decoded_row_count = 0; |
| avifResult next_image_result = use_nth_image_api |
| ? avifDecoderNextImage(decoder) |
| : avifDecoderNextImage(decoder); |
| while (next_image_result == AVIF_RESULT_WAITING_ON_IO) { |
| if (data.available.size >= data.full_size) { |
| std::cerr << (use_nth_image_api ? "avifDecoderNthImage(0)" |
| : "avifDecoderNextImage()") |
| << " returned WAITING_ON_IO instead of OK"; |
| return AVIF_RESULT_INVALID_ARGUMENT; |
| } |
| const uint32_t decoded_row_count = avifDecoderDecodedRowCount(decoder); |
| EXPECT_GE(decoded_row_count, previously_decoded_row_count); |
| const uint32_t min_decoded_row_count = GetMinDecodedRowCount( |
| reference.height, cell_height, reference.alphaPlane != nullptr, |
| data.available.size, data.full_size, enable_fine_incremental_check); |
| EXPECT_GE(decoded_row_count, min_decoded_row_count); |
| if (decoded_row_count > 0) { |
| ComparePartialYuva(reference, *decoder->image, decoded_row_count); |
| } |
| |
| previously_decoded_row_count = decoded_row_count; |
| data.available.size = std::min(data.available.size + step, data.full_size); |
| next_image_result = use_nth_image_api ? avifDecoderNextImage(decoder) |
| : avifDecoderNextImage(decoder); |
| } |
| EXPECT_EQ(next_image_result, AVIF_RESULT_OK); |
| if (expect_whole_file_read) { |
| EXPECT_EQ(data.available.size, data.full_size); |
| } |
| EXPECT_EQ(avifDecoderDecodedRowCount(decoder), decoder->image->height); |
| |
| ComparePartialYuva(reference, *decoder->image, reference.height); |
| return AVIF_RESULT_OK; |
| } |
| |
| std::string get_file_name(const char* file_name) { |
| return std::string(data_path) + file_name; |
| } |
| |
| // Check that non-incremental and incremental decodings of a grid AVIF produce |
| // the same pixels. |
| TEST(IncrementalTest, Decode) { |
| auto file_data = |
| testutil::read_file(get_file_name("sofa_grid1x5_420.avif").c_str()); |
| avifRWData encoded_avif = {.data = file_data.data(), |
| .size = file_data.size()}; |
| ASSERT_NE(encoded_avif.size, 0u); |
| ImagePtr reference(avifImageCreateEmpty()); |
| ASSERT_NE(reference, nullptr); |
| DecoderPtr decoder(avifDecoderCreate()); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadMemory(decoder.get(), reference.get(), |
| encoded_avif.data, encoded_avif.size), |
| AVIF_RESULT_OK); |
| |
| DecoderPtr decoder2(avifDecoderCreate()); |
| ASSERT_NE(decoder2, nullptr); |
| |
| // Cell height is hardcoded because there is no API to extract it from an |
| // encoded payload. |
| ASSERT_EQ(DecodeIncrementally(encoded_avif, decoder2.get(), |
| /*is_persistent=*/true, /*give_size_hint=*/true, |
| /*use_nth_image_api=*/false, *reference, |
| /*cell_height=*/154, |
| /*enable_fine_incremental_check=*/true, true), |
| AVIF_RESULT_OK); |
| } |
| |
| } // namespace |
| } // namespace avif |
| |
| int main(int argc, char** argv) { |
| ::testing::InitGoogleTest(&argc, argv); |
| if (argc < 2) { |
| std::cerr |
| << "The path to the test data folder must be provided as an argument" |
| << std::endl; |
| return 1; |
| } |
| avif::data_path = argv[1]; |
| return RUN_ALL_TESTS(); |
| } |