| // Copyright 2022 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "aviftest_helpers.h" |
| |
| #include <algorithm> |
| #include <cassert> |
| #include <cmath> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <cstring> |
| #include <fstream> |
| #include <limits> |
| #include <string> |
| #include <vector> |
| |
| #include "avif/avif.h" |
| #include "avif/avif_cxx.h" |
| #include "avifpng.h" |
| #include "avifutil.h" |
| |
| namespace avif { |
| namespace testutil { |
| |
| //------------------------------------------------------------------------------ |
| // CopyImageSamples is a copy of avifImageCopySamples |
| |
| namespace { |
| void CopyImageSamples(avifImage* dstImage, const avifImage* srcImage, |
| avifPlanesFlags planes) { |
| assert(srcImage->depth == dstImage->depth); |
| if (planes & AVIF_PLANES_YUV) { |
| assert(srcImage->yuvFormat == dstImage->yuvFormat); |
| // Note that there may be a mismatch between srcImage->yuvRange and |
| // dstImage->yuvRange because libavif allows for 'colr' and AV1 OBU video |
| // range values to differ. |
| } |
| const size_t bytesPerPixel = avifImageUsesU16(srcImage) ? 2 : 1; |
| |
| const avifBool skipColor = !(planes & AVIF_PLANES_YUV); |
| const avifBool skipAlpha = !(planes & AVIF_PLANES_A); |
| for (int c = AVIF_CHAN_Y; c <= AVIF_CHAN_A; ++c) { |
| const avifBool alpha = c == AVIF_CHAN_A; |
| if ((skipColor && !alpha) || (skipAlpha && alpha)) { |
| continue; |
| } |
| |
| const uint32_t planeWidth = avifImagePlaneWidth(srcImage, c); |
| const uint32_t planeHeight = avifImagePlaneHeight(srcImage, c); |
| const uint8_t* srcRow = avifImagePlane(srcImage, c); |
| uint8_t* dstRow = avifImagePlane(dstImage, c); |
| const uint32_t srcRowBytes = avifImagePlaneRowBytes(srcImage, c); |
| const uint32_t dstRowBytes = avifImagePlaneRowBytes(dstImage, c); |
| assert(!srcRow == !dstRow); |
| if (!srcRow) { |
| continue; |
| } |
| assert(planeWidth == avifImagePlaneWidth(dstImage, c)); |
| assert(planeHeight == avifImagePlaneHeight(dstImage, c)); |
| |
| const size_t planeWidthBytes = planeWidth * bytesPerPixel; |
| for (uint32_t y = 0; y < planeHeight; ++y) { |
| memcpy(dstRow, srcRow, planeWidthBytes); |
| srcRow += srcRowBytes; |
| dstRow += dstRowBytes; |
| } |
| } |
| } |
| } // namespace |
| |
| //------------------------------------------------------------------------------ |
| |
| AvifRgbImage::AvifRgbImage(const avifImage* yuv, int rgbDepth, |
| avifRGBFormat rgbFormat) { |
| avifRGBImageSetDefaults(this, yuv); |
| depth = rgbDepth; |
| format = rgbFormat; |
| if (avifRGBImageAllocatePixels(this) != AVIF_RESULT_OK) { |
| std::abort(); |
| } |
| } |
| |
| AvifRwData::AvifRwData(AvifRwData&& other) : avifRWData{other} { |
| other.data = nullptr; |
| other.size = 0; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| RgbChannelOffsets GetRgbChannelOffsets(avifRGBFormat format) { |
| switch (format) { |
| case AVIF_RGB_FORMAT_RGB: |
| return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/0}; |
| case AVIF_RGB_FORMAT_RGBA: |
| return {/*r=*/0, /*g=*/1, /*b=*/2, /*a=*/3}; |
| case AVIF_RGB_FORMAT_ARGB: |
| return {/*r=*/1, /*g=*/2, /*b=*/3, /*a=*/0}; |
| case AVIF_RGB_FORMAT_BGR: |
| return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/0}; |
| case AVIF_RGB_FORMAT_BGRA: |
| return {/*r=*/2, /*g=*/1, /*b=*/0, /*a=*/3}; |
| case AVIF_RGB_FORMAT_ABGR: |
| return {/*r=*/3, /*g=*/2, /*b=*/1, /*a=*/0}; |
| case AVIF_RGB_FORMAT_RGB_565: |
| case AVIF_RGB_FORMAT_COUNT: |
| default: |
| return {/*r=*/0, /*g=*/0, /*b=*/0, /*a=*/0}; |
| } |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| ImagePtr CreateImage(int width, int height, int depth, |
| avifPixelFormat yuv_format, avifPlanesFlags planes, |
| avifRange yuv_range) { |
| ImagePtr image(avifImageCreate(width, height, depth, yuv_format)); |
| if (!image) { |
| return nullptr; |
| } |
| image->yuvRange = yuv_range; |
| if (avifImageAllocatePlanes(image.get(), planes) != AVIF_RESULT_OK) { |
| return nullptr; |
| } |
| return image; |
| } |
| |
| void FillImagePlain(avifImage* image, const uint32_t yuva[4]) { |
| for (avifChannelIndex c : |
| {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
| const uint32_t plane_width = avifImagePlaneWidth(image, c); |
| // 0 for A if no alpha and 0 for UV if 4:0:0. |
| const uint32_t plane_height = avifImagePlaneHeight(image, c); |
| uint8_t* row = avifImagePlane(image, c); |
| const uint32_t row_bytes = avifImagePlaneRowBytes(image, c); |
| for (uint32_t y = 0; y < plane_height; ++y) { |
| if (avifImageUsesU16(image)) { |
| std::fill(reinterpret_cast<uint16_t*>(row), |
| reinterpret_cast<uint16_t*>(row) + plane_width, |
| static_cast<uint16_t>(yuva[c])); |
| } else { |
| std::fill(row, row + plane_width, static_cast<uint8_t>(yuva[c])); |
| } |
| row += row_bytes; |
| } |
| } |
| } |
| |
| void FillImageGradient(avifImage* image) { |
| for (avifChannelIndex c : |
| {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
| const uint32_t limitedRangeMin = |
| c == AVIF_CHAN_Y ? 16 << (image->depth - 8) : 0; |
| const uint32_t limitedRangeMax = (c == AVIF_CHAN_Y ? 219 : 224) |
| << (image->depth - 8); |
| |
| const uint32_t plane_width = avifImagePlaneWidth(image, c); |
| // 0 for A if no alpha and 0 for UV if 4:0:0. |
| const uint32_t plane_height = avifImagePlaneHeight(image, c); |
| uint8_t* row = avifImagePlane(image, c); |
| const uint32_t row_bytes = avifImagePlaneRowBytes(image, c); |
| for (uint32_t y = 0; y < plane_height; ++y) { |
| for (uint32_t x = 0; x < plane_width; ++x) { |
| uint32_t value; |
| if (image->yuvRange == AVIF_RANGE_FULL || c == AVIF_CHAN_A) { |
| value = (x + y) * ((1u << image->depth) - 1u) / |
| std::max(1u, plane_width + plane_height - 2); |
| } else { |
| value = limitedRangeMin + |
| (x + y) * (limitedRangeMax - limitedRangeMin) / |
| std::max(1u, plane_width + plane_height - 2); |
| } |
| if (avifImageUsesU16(image)) { |
| reinterpret_cast<uint16_t*>(row)[x] = static_cast<uint16_t>(value); |
| } else { |
| row[x] = static_cast<uint8_t>(value); |
| } |
| } |
| row += row_bytes; |
| } |
| } |
| } |
| |
| namespace { |
| template <typename PixelType> |
| void FillImageChannel(avifRGBImage* image, uint32_t channel_offset, |
| uint32_t value) { |
| const uint32_t channel_count = avifRGBFormatChannelCount(image->format); |
| assert(channel_offset < channel_count); |
| for (uint32_t y = 0; y < image->height; ++y) { |
| PixelType* pixel = |
| reinterpret_cast<PixelType*>(image->pixels + image->rowBytes * y); |
| for (uint32_t x = 0; x < image->width; ++x) { |
| pixel[channel_offset] = static_cast<PixelType>(value); |
| pixel += channel_count; |
| } |
| } |
| } |
| } // namespace |
| |
| void FillImageChannel(avifRGBImage* image, uint32_t channel_offset, |
| uint32_t value) { |
| (image->depth <= 8) |
| ? FillImageChannel<uint8_t>(image, channel_offset, value) |
| : FillImageChannel<uint16_t>(image, channel_offset, value); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| bool AreByteSequencesEqual(const uint8_t data1[], size_t data1_length, |
| const uint8_t data2[], size_t data2_length) { |
| if (data1_length != data2_length) return false; |
| return data1_length == 0 || std::equal(data1, data1 + data1_length, data2); |
| } |
| |
| bool AreByteSequencesEqual(const avifRWData& data1, const avifRWData& data2) { |
| return AreByteSequencesEqual(data1.data, data1.size, data2.data, data2.size); |
| } |
| |
| // Returns true if image1 and image2 are identical. |
| bool AreImagesEqual(const avifImage& image1, const avifImage& image2, |
| bool ignore_alpha) { |
| if (image1.width != image2.width || image1.height != image2.height || |
| image1.depth != image2.depth || image1.yuvFormat != image2.yuvFormat || |
| image1.yuvRange != image2.yuvRange) { |
| return false; |
| } |
| assert(image1.width * image1.height > 0); |
| |
| if (image1.clli.maxCLL != image2.clli.maxCLL || |
| image1.clli.maxPALL != image2.clli.maxPALL) { |
| return false; |
| } |
| if (image1.transformFlags != image2.transformFlags || |
| ((image1.transformFlags & AVIF_TRANSFORM_PASP) && |
| std::memcmp(&image1.pasp, &image2.pasp, sizeof(image1.pasp))) || |
| ((image1.transformFlags & AVIF_TRANSFORM_CLAP) && |
| std::memcmp(&image1.clap, &image2.clap, sizeof(image1.clap))) || |
| ((image1.transformFlags & AVIF_TRANSFORM_IROT) && |
| std::memcmp(&image1.irot, &image2.irot, sizeof(image1.irot))) || |
| ((image1.transformFlags & AVIF_TRANSFORM_IMIR) && |
| std::memcmp(&image1.imir, &image2.imir, sizeof(image1.imir)))) { |
| return false; |
| } |
| |
| for (avifChannelIndex c : |
| {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
| if (ignore_alpha && c == AVIF_CHAN_A) continue; |
| const uint8_t* row1 = avifImagePlane(&image1, c); |
| const uint8_t* row2 = avifImagePlane(&image2, c); |
| if (!row1 != !row2) { |
| // Maybe one image contains an opaque alpha channel while the other has no |
| // alpha channel, but they should still be considered equal. |
| if (c == AVIF_CHAN_A && avifImageIsOpaque(&image1) && |
| avifImageIsOpaque(&image2)) { |
| continue; |
| } |
| return false; |
| } |
| const uint32_t row_bytes1 = avifImagePlaneRowBytes(&image1, c); |
| const uint32_t row_bytes2 = avifImagePlaneRowBytes(&image2, c); |
| const uint32_t plane_width = avifImagePlaneWidth(&image1, c); |
| // 0 for A if no alpha and 0 for UV if 4:0:0. |
| const uint32_t plane_height = avifImagePlaneHeight(&image1, c); |
| for (uint32_t y = 0; y < plane_height; ++y) { |
| if (avifImageUsesU16(&image1)) { |
| if (!std::equal(reinterpret_cast<const uint16_t*>(row1), |
| reinterpret_cast<const uint16_t*>(row1) + plane_width, |
| reinterpret_cast<const uint16_t*>(row2))) { |
| return false; |
| } |
| } else { |
| if (!std::equal(row1, row1 + plane_width, row2)) { |
| return false; |
| } |
| } |
| row1 += row_bytes1; |
| row2 += row_bytes2; |
| } |
| } |
| return AreByteSequencesEqual(image1.icc, image2.icc) && |
| AreByteSequencesEqual(image1.exif, image2.exif) && |
| AreByteSequencesEqual(image1.xmp, image2.xmp); |
| } |
| |
| namespace { |
| |
| template <typename Sample> |
| uint64_t SquaredDiffSum(const Sample* samples1, const Sample* samples2, |
| uint32_t num_samples) { |
| uint64_t sum = 0; |
| for (uint32_t i = 0; i < num_samples; ++i) { |
| const int32_t diff = static_cast<int32_t>(samples1[i]) - samples2[i]; |
| sum += diff * diff; |
| } |
| return sum; |
| } |
| |
| } // namespace |
| |
| double GetPsnr(const avifImage& image1, const avifImage& image2, |
| bool ignore_alpha) { |
| if (image1.width != image2.width || image1.height != image2.height || |
| image1.depth != image2.depth || image1.yuvFormat != image2.yuvFormat || |
| image1.yuvRange != image2.yuvRange) { |
| return -1; |
| } |
| assert(image1.width * image1.height > 0); |
| |
| if (image1.colorPrimaries != image2.colorPrimaries || |
| image1.transferCharacteristics != image2.transferCharacteristics || |
| image1.matrixCoefficients != image2.matrixCoefficients || |
| image1.yuvRange != image2.yuvRange) { |
| fprintf(stderr, |
| "WARNING: computing PSNR of images with different CICP: %d/%d/%d%s " |
| "vs %d/%d/%d%s\n", |
| image1.colorPrimaries, image1.transferCharacteristics, |
| image1.matrixCoefficients, |
| (image1.yuvRange == AVIF_RANGE_FULL) ? "f" : "l", |
| image2.colorPrimaries, image2.transferCharacteristics, |
| image2.matrixCoefficients, |
| (image2.yuvRange == AVIF_RANGE_FULL) ? "f" : "l"); |
| } |
| |
| uint64_t squared_diff_sum = 0; |
| uint32_t num_samples = 0; |
| const uint32_t max_sample_value = (1 << image1.depth) - 1; |
| for (avifChannelIndex c : |
| {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { |
| if (ignore_alpha && c == AVIF_CHAN_A) continue; |
| |
| const uint32_t plane_width = std::max(avifImagePlaneWidth(&image1, c), |
| avifImagePlaneWidth(&image2, c)); |
| const uint32_t plane_height = std::max(avifImagePlaneHeight(&image1, c), |
| avifImagePlaneHeight(&image2, c)); |
| if (plane_width == 0 || plane_height == 0) continue; |
| |
| const uint8_t* row1 = avifImagePlane(&image1, c); |
| const uint8_t* row2 = avifImagePlane(&image2, c); |
| if (!row1 != !row2 && c != AVIF_CHAN_A) { |
| return -1; |
| } |
| uint32_t row_bytes1 = avifImagePlaneRowBytes(&image1, c); |
| uint32_t row_bytes2 = avifImagePlaneRowBytes(&image2, c); |
| |
| // Consider missing alpha planes as samples set to the maximum value. |
| std::vector<uint8_t> opaque_alpha_samples; |
| if (!row1 != !row2) { |
| opaque_alpha_samples.resize(std::max(row_bytes1, row_bytes2)); |
| if (avifImageUsesU16(&image1)) { |
| uint16_t* opaque_alpha_samples_16b = |
| reinterpret_cast<uint16_t*>(opaque_alpha_samples.data()); |
| std::fill(opaque_alpha_samples_16b, |
| opaque_alpha_samples_16b + plane_width, |
| static_cast<int16_t>(max_sample_value)); |
| } else { |
| std::fill(opaque_alpha_samples.begin(), opaque_alpha_samples.end(), |
| uint8_t{255}); |
| } |
| if (!row1) { |
| row1 = opaque_alpha_samples.data(); |
| row_bytes1 = 0; |
| } else { |
| row2 = opaque_alpha_samples.data(); |
| row_bytes2 = 0; |
| } |
| } |
| |
| for (uint32_t y = 0; y < plane_height; ++y) { |
| if (avifImageUsesU16(&image1)) { |
| squared_diff_sum += SquaredDiffSum( |
| reinterpret_cast<const uint16_t*>(row1), |
| reinterpret_cast<const uint16_t*>(row2), plane_width); |
| } else { |
| squared_diff_sum += SquaredDiffSum(row1, row2, plane_width); |
| } |
| row1 += row_bytes1; |
| row2 += row_bytes2; |
| num_samples += plane_width; |
| } |
| } |
| |
| if (squared_diff_sum == 0) { |
| return 99.0; |
| } |
| const double normalized_error = |
| squared_diff_sum / |
| (static_cast<double>(num_samples) * max_sample_value * max_sample_value); |
| if (normalized_error <= std::numeric_limits<double>::epsilon()) { |
| return 98.99; // Very small distortion but not lossless. |
| } |
| return std::min(-10 * std::log10(normalized_error), 98.99); |
| } |
| |
| bool AreImagesEqual(const avifRGBImage& image1, const avifRGBImage& image2) { |
| if (image1.width != image2.width || image1.height != image2.height || |
| image1.depth != image2.depth || image1.format != image2.format || |
| image1.alphaPremultiplied != image2.alphaPremultiplied || |
| image1.isFloat != image2.isFloat) { |
| return false; |
| } |
| const uint8_t* row1 = image1.pixels; |
| const uint8_t* row2 = image2.pixels; |
| const unsigned int row_width = image1.width * avifRGBImagePixelSize(&image1); |
| for (unsigned int y = 0; y < image1.height; ++y) { |
| if (!std::equal(row1, row1 + row_width, row2)) { |
| return false; |
| } |
| row1 += image1.rowBytes; |
| row2 += image2.rowBytes; |
| } |
| return true; |
| } |
| |
| avifResult MergeGrid(int grid_cols, int grid_rows, |
| const std::vector<ImagePtr>& cells, avifImage* merged) { |
| std::vector<const avifImage*> ptrs(cells.size()); |
| for (size_t i = 0; i < cells.size(); ++i) { |
| ptrs[i] = cells[i].get(); |
| } |
| return MergeGrid(grid_cols, grid_rows, ptrs, merged); |
| } |
| |
| avifResult MergeGrid(int grid_cols, int grid_rows, |
| const std::vector<const avifImage*>& cells, |
| avifImage* merged) { |
| const uint32_t tile_width = cells[0]->width; |
| const uint32_t tile_height = cells[0]->height; |
| const uint32_t grid_width = |
| (grid_cols - 1) * tile_width + cells.back()->width; |
| const uint32_t grid_height = |
| (grid_rows - 1) * tile_height + cells.back()->height; |
| |
| ImagePtr view(avifImageCreateEmpty()); |
| AVIF_CHECKERR(view, AVIF_RESULT_OUT_OF_MEMORY); |
| |
| avifCropRect rect = {}; |
| for (int j = 0; j < grid_rows; ++j) { |
| rect.x = 0; |
| for (int i = 0; i < grid_cols; ++i) { |
| const avifImage* image = cells[j * grid_cols + i]; |
| rect.width = image->width; |
| rect.height = image->height; |
| AVIF_CHECKRES(avifImageSetViewRect(view.get(), merged, &rect)); |
| CopyImageSamples(/*dstImage=*/view.get(), image, AVIF_PLANES_ALL); |
| assert(!view->imageOwnsYUVPlanes); |
| rect.x += rect.width; |
| } |
| rect.y += rect.height; |
| } |
| |
| if ((rect.x != grid_width) || (rect.y != grid_height)) { |
| return AVIF_RESULT_UNKNOWN_ERROR; |
| } |
| |
| return AVIF_RESULT_OK; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| testutil::AvifRwData ReadFile(const std::string& file_path) { |
| std::ifstream file(file_path, std::ios::binary | std::ios::ate); |
| testutil::AvifRwData bytes; |
| if (avifRWDataRealloc(&bytes, file.good() ? static_cast<size_t>(file.tellg()) |
| : 0) != AVIF_RESULT_OK) { |
| return {}; |
| } |
| file.seekg(0, std::ios::beg); |
| file.read(reinterpret_cast<char*>(bytes.data), |
| static_cast<std::streamsize>(bytes.size)); |
| return bytes; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| ImagePtr ReadImage(const char* folder_path, const char* file_name, |
| avifPixelFormat requested_format, int requested_depth, |
| avifChromaDownsampling chroma_downsampling, |
| avifBool ignore_icc, avifBool ignore_exif, |
| avifBool ignore_xmp, avifBool allow_changing_cicp, |
| avifBool ignore_gain_map) { |
| ImagePtr image(avifImageCreateEmpty()); |
| if (!image || |
| avifReadImage((std::string(folder_path) + file_name).c_str(), |
| requested_format, requested_depth, chroma_downsampling, |
| ignore_icc, ignore_exif, ignore_xmp, allow_changing_cicp, |
| ignore_gain_map, AVIF_DEFAULT_IMAGE_SIZE_LIMIT, image.get(), |
| /*outDepth=*/nullptr, /*sourceTiming=*/nullptr, |
| /*frameIter=*/nullptr) == AVIF_APP_FILE_FORMAT_UNKNOWN) { |
| return nullptr; |
| } |
| return image; |
| } |
| |
| bool WriteImage(const avifImage* image, const char* file_path) { |
| if (!image || !file_path) return false; |
| const size_t str_len = std::strlen(file_path); |
| if (str_len >= 4 && !std::strncmp(file_path + str_len - 4, ".png", 4)) { |
| return avifPNGWrite(file_path, image, /*requestedDepth=*/0, |
| AVIF_CHROMA_UPSAMPLING_BEST_QUALITY, |
| /*compressionLevel=*/0); |
| } |
| // Other formats are not supported. |
| return false; |
| } |
| |
| AvifRwData Encode(const avifImage* image, int speed, int quality) { |
| EncoderPtr encoder(avifEncoderCreate()); |
| if (!encoder) return {}; |
| encoder->speed = speed; |
| encoder->quality = quality; |
| encoder->qualityAlpha = quality; |
| #if defined(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) |
| encoder->qualityGainMap = quality; |
| #endif |
| testutil::AvifRwData bytes; |
| if (avifEncoderWrite(encoder.get(), image, &bytes) != AVIF_RESULT_OK) { |
| return {}; |
| } |
| return bytes; |
| } |
| |
| ImagePtr Decode(const uint8_t* bytes, size_t num_bytes) { |
| ImagePtr decoded(avifImageCreateEmpty()); |
| DecoderPtr decoder(avifDecoderCreate()); |
| if (!decoded || !decoder || |
| (avifDecoderReadMemory(decoder.get(), decoded.get(), bytes, num_bytes) != |
| AVIF_RESULT_OK)) { |
| return nullptr; |
| } |
| return decoded; |
| } |
| |
| ImagePtr DecodeFile(const std::string& path) { |
| ImagePtr decoded(avifImageCreateEmpty()); |
| DecoderPtr decoder(avifDecoderCreate()); |
| if (!decoded || !decoder || |
| (avifDecoderReadFile(decoder.get(), decoded.get(), path.c_str()) != |
| AVIF_RESULT_OK)) { |
| return nullptr; |
| } |
| return decoded; |
| } |
| |
| bool Av1EncoderAvailable() { |
| const char* encoding_codec = |
| avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_ENCODE); |
| return encoding_codec != nullptr && std::string(encoding_codec) != "avm"; |
| } |
| |
| bool Av1DecoderAvailable() { |
| const char* decoding_codec = |
| avifCodecName(AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_FLAG_CAN_DECODE); |
| return decoding_codec != nullptr && std::string(decoding_codec) != "avm"; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| static avifResult avifIOLimitedReaderRead(avifIO* io, uint32_t readFlags, |
| uint64_t offset, size_t size, |
| avifROData* out) { |
| auto reader = reinterpret_cast<AvifIOLimitedReader*>(io); |
| |
| if (offset > UINT64_MAX - size) { |
| return AVIF_RESULT_IO_ERROR; |
| } |
| if (offset + size > reader->clamp) { |
| return AVIF_RESULT_WAITING_ON_IO; |
| } |
| |
| return reader->underlyingIO->read(reader->underlyingIO, readFlags, offset, |
| size, out); |
| } |
| |
| static void avifIOLimitedReaderDestroy(avifIO* io) { |
| auto reader = reinterpret_cast<AvifIOLimitedReader*>(io); |
| reader->underlyingIO->destroy(reader->underlyingIO); |
| delete reader; |
| } |
| |
| avifIO* AvifIOCreateLimitedReader(avifIO* underlyingIO, uint64_t clamp) { |
| return reinterpret_cast<avifIO*>( |
| new AvifIOLimitedReader{{ |
| avifIOLimitedReaderDestroy, |
| avifIOLimitedReaderRead, |
| nullptr, |
| underlyingIO->sizeHint, |
| underlyingIO->persistent, |
| nullptr, |
| }, |
| underlyingIO, |
| clamp}); |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| std::vector<ImagePtr> ImageToGrid(const avifImage* image, uint32_t grid_cols, |
| uint32_t grid_rows) { |
| if (image->width < grid_cols || image->height < grid_rows) return {}; |
| |
| // Round up, to make sure all samples are used by exactly one cell. |
| uint32_t cell_width = (image->width + grid_cols - 1) / grid_cols; |
| uint32_t cell_height = (image->height + grid_rows - 1) / grid_rows; |
| |
| if ((grid_cols - 1) * cell_width >= image->width) { |
| // Some cells are completely outside the image. Fallback to a grid entirely |
| // contained within the image boundaries. Some samples will be discarded but |
| // at least the test can go on. |
| cell_width = image->width / grid_cols; |
| } |
| if ((grid_rows - 1) * cell_height >= image->height) { |
| cell_height = image->height / grid_rows; |
| } |
| |
| std::vector<ImagePtr> cells; |
| for (uint32_t row = 0; row < grid_rows; ++row) { |
| for (uint32_t col = 0; col < grid_cols; ++col) { |
| avifCropRect rect{col * cell_width, row * cell_height, cell_width, |
| cell_height}; |
| assert(rect.x < image->width); |
| assert(rect.y < image->height); |
| // The right-most and bottom-most cells may be smaller than others. |
| // The encoder will pad them. |
| if (rect.x + rect.width > image->width) { |
| rect.width = image->width - rect.x; |
| } |
| if (rect.y + rect.height > image->height) { |
| rect.height = image->height - rect.y; |
| } |
| cells.emplace_back(avifImageCreateEmpty()); |
| if (avifImageSetViewRect(cells.back().get(), image, &rect) != |
| AVIF_RESULT_OK) { |
| return {}; |
| } |
| } |
| } |
| return cells; |
| } |
| |
| std::vector<const avifImage*> UniquePtrToRawPtr( |
| const std::vector<ImagePtr>& unique_ptrs) { |
| std::vector<const avifImage*> rawPtrs; |
| rawPtrs.reserve(unique_ptrs.size()); |
| for (const ImagePtr& unique_ptr : unique_ptrs) { |
| rawPtrs.emplace_back(unique_ptr.get()); |
| } |
| return rawPtrs; |
| } |
| |
| //------------------------------------------------------------------------------ |
| |
| } // namespace testutil |
| } // namespace avif |