| // Copyright 2022 Google LLC. All rights reserved. |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avifincrtest_helpers.h" |
| |
| #include <algorithm> |
| #include <cstring> |
| #include <vector> |
| |
| #include "avif/avif.h" |
| #include "aviftest_helpers.h" |
| #include "gtest/gtest.h" |
| |
| namespace libavif { |
| namespace testutil { |
| namespace { |
| |
| //------------------------------------------------------------------------------ |
| |
| // 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_width = |
| (image1.width + info.chromaShiftX) >> info.chromaShiftX; |
| const uint32_t uv_height = |
| (row_count + info.chromaShiftY) >> info.chromaShiftY; |
| const uint32_t pixel_byte_count = |
| (image1.depth > 8) ? sizeof(uint16_t) : sizeof(uint8_t); |
| |
| for (int plane = 0; plane < (info.monochrome ? 1 : AVIF_PLANE_COUNT_YUV); |
| ++plane) { |
| const uint32_t width = (plane == AVIF_CHAN_Y) ? image1.width : uv_width; |
| const uint32_t width_byte_count = width * pixel_byte_count; |
| const uint32_t height = (plane == AVIF_CHAN_Y) ? row_count : uv_height; |
| const uint8_t* data1 = image1.yuvPlanes[plane]; |
| const uint8_t* data2 = image2.yuvPlanes[plane]; |
| for (uint32_t y = 0; y < height; ++y) { |
| ASSERT_EQ(std::memcmp(data1, data2, width_byte_count), 0); |
| data1 += image1.yuvRowBytes[plane]; |
| data2 += image2.yuvRowBytes[plane]; |
| } |
| } |
| |
| if (image1.alphaPlane) { |
| ASSERT_NE(image2.alphaPlane, nullptr); |
| ASSERT_EQ(image1.alphaPremultiplied, image2.alphaPremultiplied); |
| const uint32_t width_byte_count = image1.width * pixel_byte_count; |
| const uint8_t* data1 = image1.alphaPlane; |
| const uint8_t* data2 = image2.alphaPlane; |
| for (uint32_t y = 0; y < row_count; ++y) { |
| ASSERT_EQ(std::memcmp(data1, data2, width_byte_count), 0); |
| data1 += image1.alphaRowBytes; |
| data2 += image2.alphaRowBytes; |
| } |
| } |
| } |
| |
| // 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) { |
| // 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; |
| } |
| |
| // 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 offset, |
| size_t size, avifROData* out) { |
| PartialData* data = reinterpret_cast<PartialData*>(io->data); |
| if ((read_flags != 0) || !data || (data->full_size < offset)) { |
| return AVIF_RESULT_IO_ERROR; |
| } |
| if (data->full_size < (offset + size)) { |
| 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; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| // Encodes the image as a grid of at most grid_cols*grid_rows cells. |
| // The cell count is reduced to fit libavif or AVIF format constraints. If |
| // impossible, the encoded output is returned empty. The final cell_width and |
| // cell_height are output. |
| void EncodeAsGrid(const avifImage& image, uint32_t grid_cols, |
| uint32_t grid_rows, avifRWData* output, uint32_t* cell_width, |
| uint32_t* cell_height) { |
| // Chroma subsampling requires even dimensions. See ISO 23000-22 - 7.3.11.4.2 |
| const bool need_even_widths = |
| ((image.yuvFormat == AVIF_PIXEL_FORMAT_YUV420) || |
| (image.yuvFormat == AVIF_PIXEL_FORMAT_YUV422)); |
| const bool need_even_heights = (image.yuvFormat == AVIF_PIXEL_FORMAT_YUV420); |
| |
| ASSERT_GT(grid_cols * grid_rows, 0u); |
| *cell_width = image.width / grid_cols; |
| *cell_height = image.height / grid_rows; |
| |
| // avifEncoderAddImageGrid() only accepts grids that evenly split the image |
| // into cells at least 64 pixels wide and tall. |
| while ((grid_cols > 1) && |
| (((*cell_width * grid_cols) != image.width) || (*cell_width < 64) || |
| (need_even_widths && ((*cell_width & 1) != 0)))) { |
| --grid_cols; |
| *cell_width = image.width / grid_cols; |
| } |
| while ((grid_rows > 1) && |
| (((*cell_height * grid_rows) != image.height) || (*cell_height < 64) || |
| (need_even_heights && ((*cell_height & 1) != 0)))) { |
| --grid_rows; |
| *cell_height = image.height / grid_rows; |
| } |
| |
| std::vector<testutil::AvifImagePtr> cell_images; |
| cell_images.reserve(grid_cols * grid_rows); |
| for (uint32_t row = 0, i_cell = 0; row < grid_rows; ++row) { |
| for (uint32_t col = 0; col < grid_cols; ++col, ++i_cell) { |
| avifCropRect cell; |
| cell.x = col * *cell_width; |
| cell.y = row * *cell_height; |
| cell.width = ((cell.x + *cell_width) <= image.width) |
| ? *cell_width |
| : (image.width - cell.x); |
| cell.height = ((cell.y + *cell_height) <= image.height) |
| ? *cell_height |
| : (image.height - cell.y); |
| cell_images.emplace_back(avifImageCreateEmpty(), avifImageDestroy); |
| ASSERT_NE(cell_images.back(), nullptr); |
| ASSERT_EQ(avifImageSetViewRect(cell_images.back().get(), &image, &cell), |
| AVIF_RESULT_OK); |
| } |
| } |
| |
| testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy); |
| ASSERT_NE(encoder, nullptr); |
| encoder->speed = AVIF_SPEED_FASTEST; |
| // Just here to match libavif API. |
| std::vector<avifImage*> cell_image_ptrs(cell_images.size()); |
| for (size_t i = 0; i < cell_images.size(); ++i) { |
| cell_image_ptrs[i] = cell_images[i].get(); |
| } |
| ASSERT_EQ(avifEncoderAddImageGrid(encoder.get(), grid_cols, grid_rows, |
| cell_image_ptrs.data(), |
| AVIF_ADD_IMAGE_FLAG_SINGLE), |
| AVIF_RESULT_OK); |
| ASSERT_EQ(avifEncoderFinish(encoder.get(), output), AVIF_RESULT_OK); |
| } |
| |
| // Encodes the image to be decoded incrementally. |
| void EncodeAsIncremental(const avifImage& image, bool flat_cells, |
| avifRWData* output, uint32_t* cell_width, |
| uint32_t* cell_height) { |
| const uint32_t grid_cols = image.width / 64; // 64px is the min cell width. |
| const uint32_t grid_rows = flat_cells ? 1 : (image.height / 64); |
| EncodeAsGrid(image, (grid_cols > 1) ? grid_cols : 1, |
| (grid_rows > 1) ? grid_rows : 1, output, cell_width, |
| cell_height); |
| } |
| |
| } // namespace |
| |
| void EncodeRectAsIncremental(const avifImage& image, uint32_t width, |
| uint32_t height, bool create_alpha_if_none, |
| bool flat_cells, avifRWData* output, |
| uint32_t* cell_width, uint32_t* cell_height) { |
| AvifImagePtr sub_image(avifImageCreateEmpty(), avifImageDestroy); |
| ASSERT_NE(sub_image, nullptr); |
| ASSERT_LE(width, image.width); |
| ASSERT_LE(height, image.height); |
| avifPixelFormatInfo info; |
| avifGetPixelFormatInfo(image.yuvFormat, &info); |
| const avifCropRect rect{ |
| /*x=*/((image.width - width) / 2) & ~info.chromaShiftX, |
| /*y=*/((image.height - height) / 2) & ~info.chromaShiftX, width, height}; |
| ASSERT_EQ(avifImageSetViewRect(sub_image.get(), &image, &rect), |
| AVIF_RESULT_OK); |
| if (create_alpha_if_none && !sub_image->alphaPlane) { |
| ASSERT_NE(image.yuvPlanes[AVIF_CHAN_Y], nullptr) |
| << "No luma plane to simulate an alpha plane"; |
| sub_image->alphaPlane = image.yuvPlanes[AVIF_CHAN_Y]; |
| sub_image->alphaRowBytes = image.yuvRowBytes[AVIF_CHAN_Y]; |
| sub_image->alphaPremultiplied = AVIF_FALSE; |
| sub_image->imageOwnsAlphaPlane = AVIF_FALSE; |
| } |
| EncodeAsIncremental(*sub_image, flat_cells, output, cell_width, cell_height); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| void DecodeIncrementally(const avifRWData& encoded_avif, bool is_persistent, |
| bool give_size_hint, bool use_nth_image_api, |
| const avifImage& reference, uint32_t cell_height) { |
| // AVIF cells are at least 64 pixels tall. |
| if (cell_height != reference.height) { |
| ASSERT_GE(cell_height, 64u); |
| } |
| |
| // 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}; |
| |
| testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); |
| ASSERT_NE(decoder, nullptr); |
| avifDecoderSetIO(decoder.get(), &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.get()); |
| while (parse_result == AVIF_RESULT_WAITING_ON_IO) { |
| ASSERT_LT(data.available.size, data.full_size) |
| << "avifDecoderParse() returned WAITING_ON_IO instead of OK"; |
| data.available.size = std::min(data.available.size + step, data.full_size); |
| parse_result = avifDecoderParse(decoder.get()); |
| } |
| ASSERT_EQ(parse_result, AVIF_RESULT_OK); |
| |
| // Decoding is incremental. |
| uint32_t previously_decoded_row_count = 0; |
| avifResult next_image_result = use_nth_image_api |
| ? avifDecoderNthImage(decoder.get(), 0) |
| : avifDecoderNextImage(decoder.get()); |
| while (next_image_result == AVIF_RESULT_WAITING_ON_IO) { |
| ASSERT_LT(data.available.size, data.full_size) |
| << (use_nth_image_api ? "avifDecoderNthImage(0)" |
| : "avifDecoderNextImage()") |
| << " returned WAITING_ON_IO instead of OK"; |
| const uint32_t decoded_row_count = |
| avifDecoderDecodedRowCount(decoder.get()); |
| ASSERT_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); |
| ASSERT_GE(decoded_row_count, min_decoded_row_count); |
| 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 |
| ? avifDecoderNthImage(decoder.get(), 0) |
| : avifDecoderNextImage(decoder.get()); |
| } |
| ASSERT_EQ(next_image_result, AVIF_RESULT_OK); |
| ASSERT_EQ(data.available.size, data.full_size); |
| ASSERT_EQ(avifDecoderDecodedRowCount(decoder.get()), decoder->image->height); |
| |
| ComparePartialYuva(reference, *decoder->image, reference.height); |
| } |
| |
| void DecodeNonIncrementallyAndIncrementally(const avifRWData& encoded_avif, |
| bool is_persistent, |
| bool give_size_hint, |
| bool use_nth_image_api, |
| uint32_t cell_height) { |
| AvifImagePtr reference(avifImageCreateEmpty(), avifImageDestroy); |
| ASSERT_NE(reference, nullptr); |
| testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); |
| ASSERT_NE(decoder, nullptr); |
| ASSERT_EQ(avifDecoderReadMemory(decoder.get(), reference.get(), |
| encoded_avif.data, encoded_avif.size), |
| AVIF_RESULT_OK); |
| |
| DecodeIncrementally(encoded_avif, is_persistent, give_size_hint, |
| use_nth_image_api, *reference, cell_height); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| } // namespace testutil |
| } // namespace libavif |