Add FuzzTest targets through AVIF_ENABLE_FUZZTEST

Experimental Linux support only for now.
AVIF_ENABLE_FUZZTEST requires AVIF_LOCAL_FUZZTEST for now.
FuzzTest targets are added for transparency and for OSS-Fuzz exposure.
The dependency on FuzzTest is added for local reproducibility
convenience. The CMake part can be improved in further commits.
diff --git a/.gitignore b/.gitignore
index c98df16..1971fe0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 /ext/aom
 /ext/avm
 /ext/dav1d
+/ext/fuzztest
 /ext/googletest
 /ext/gpac
 /ext/libjpeg
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 78d8a94..9ed40c5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -66,6 +66,9 @@
 option(AVIF_LOCAL_GTEST
        "Build the GoogleTest framework by providing your own copy of the repo in ext/googletest (see Local Builds in README)" OFF
 )
+option(AVIF_LOCAL_FUZZTEST
+       "Build the Google FuzzTest framework by providing your own copy of the repo in ext/fuzztest (see Local Builds in README)" OFF
+)
 option(AVIF_LOCAL_AVM "Build the AVM (AV2) codec by providing your own copy of the repo in ext/avm (see Local Builds in README)"
        OFF
 )
@@ -646,6 +649,9 @@
 option(AVIF_ENABLE_GTEST
        "Build avif C++ tests, which depend on GoogleTest. Requires GoogleTest. Has no effect unless AVIF_BUILD_TESTS is ON." ON
 )
+option(AVIF_ENABLE_FUZZTEST
+       "Build avif fuzztest targets. Requires Google FuzzTest. Has no effect unless AVIF_BUILD_TESTS and AVIF_ENABLE_GTEST are ON." OFF
+)
 
 if(AVIF_BUILD_APPS OR (AVIF_BUILD_TESTS AND AVIF_ENABLE_GTEST))
     if(NOT AVIF_LOCAL_ZLIBPNG)
diff --git a/ext/fuzztest.cmd b/ext/fuzztest.cmd
new file mode 100755
index 0000000..bd72b61
--- /dev/null
+++ b/ext/fuzztest.cmd
@@ -0,0 +1,19 @@
+: # If you want to use a local build of fuzztest, you must clone the fuzztest repo in this directory.
+
+: # The odd choice of comment style in this file is to try to share this script between *nix and win32.
+
+: # cmake must be in your PATH.
+
+: # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir):
+: #     "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat"
+
+git clone https://github.com/google/fuzztest.git
+cd fuzztest
+: # There is no tagged release as of 2023/09/18. Pick the last commit.
+git checkout 867d851afd3d77bbc0067d4007360a1d9e826a22
+
+mkdir build.libavif
+cd build.libavif
+cmake -G Ninja -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ ..
+ninja
+cd ../..
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index d82fd56..39c2aa5 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -43,8 +43,7 @@
     enable_language(CXX)
     set(CMAKE_CXX_STANDARD 14)
     add_library(aviftest_helpers OBJECT gtest/aviftest_helpers.cc)
-    target_link_libraries(aviftest_helpers avif_apps)
-    target_link_libraries(aviftest_helpers avif_internal)
+    target_link_libraries(aviftest_helpers avif_apps avif_internal)
 endif()
 
 # Fuzz target without any fuzzing engine dependency. For easy reproduction of oss-fuzz issues.
@@ -255,6 +254,53 @@
 endif()
 
 ################################################################################
+# Experimental FuzzTest support (Linux only)
+
+if(AVIF_ENABLE_GTEST AND AVIF_ENABLE_FUZZTEST)
+    if(AVIF_LOCAL_FUZZTEST)
+        # Run ext/fuzztest.cmd first.
+        # Recommended top-level CMake options:
+        #   -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DAVIF_ENABLE_WERROR=OFF
+        # Reproducing a failure can be done by setting the environment variable
+        #   FUZZTEST_REPLAY=/path/to/repro_file.test
+        # and running one of the targets below.
+        # See https://github.com/google/fuzztest/blob/main/doc/quickstart-cmake.md
+        add_subdirectory(${CMAKE_SOURCE_DIR}/ext/fuzztest ${CMAKE_SOURCE_DIR}/ext/fuzztest/build.libavif EXCLUDE_FROM_ALL)
+        # FuzzTest bundles GoogleTest so no need for the targets below to link to GTEST_BOTH_LIBRARIES.
+    else()
+        message(FATAL_ERROR "fuzztest: Installed FuzzTest is not supported, please set AVIF_LOCAL_FUZZTEST=ON or AVIF_ENABLE_FUZZTEST=OFF")
+    endif()
+
+    add_executable(avif_fuzztest_dec_incr gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_dec_incr.cc)
+    target_link_libraries(avif_fuzztest_dec_incr PRIVATE avifincrtest_helpers)
+    link_fuzztest(avif_fuzztest_dec_incr)
+    gtest_discover_tests(avif_fuzztest_dec_incr)
+    add_test(NAME avif_fuzztest_dec_incr COMMAND avif_fuzztest_dec_incr)
+
+    add_executable(avif_fuzztest_enc_dec gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec.cc)
+    target_link_libraries(avif_fuzztest_enc_dec PRIVATE aviftest_helpers)
+    link_fuzztest(avif_fuzztest_enc_dec)
+    gtest_discover_tests(avif_fuzztest_enc_dec)
+    add_test(NAME avif_fuzztest_enc_dec COMMAND avif_fuzztest_enc_dec)
+
+    if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP)
+        add_executable(avif_fuzztest_enc_dec_experimental gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec_experimental.cc)
+        target_link_libraries(avif_fuzztest_enc_dec_experimental PRIVATE aviftest_helpers)
+        link_fuzztest(avif_fuzztest_enc_dec_experimental)
+        gtest_discover_tests(avif_fuzztest_enc_dec_experimental)
+        add_test(NAME avif_fuzztest_enc_dec_experimental COMMAND avif_fuzztest_enc_dec_experimental)
+    endif()
+
+    add_executable(avif_fuzztest_enc_dec_incr gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec_incr.cc)
+    target_link_libraries(avif_fuzztest_enc_dec_incr PRIVATE aviftest_helpers avifincrtest_helpers)
+    link_fuzztest(avif_fuzztest_enc_dec_incr)
+    gtest_discover_tests(avif_fuzztest_enc_dec_incr)
+    add_test(NAME avif_fuzztest_enc_dec_incr COMMAND avif_fuzztest_enc_dec_incr)
+elseif(AVIF_ENABLE_GTEST)
+    message(STATUS "FuzzTest targets are disabled because AVIF_ENABLE_FUZZTEST is OFF.")
+endif()
+
+################################################################################
 # Bash tests
 
 if(AVIF_BUILD_APPS)
diff --git a/tests/gtest/avif_fuzztest_dec_incr.cc b/tests/gtest/avif_fuzztest_dec_incr.cc
new file mode 100644
index 0000000..5fc2cf3
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_dec_incr.cc
@@ -0,0 +1,98 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+// Compare non-incremental and incremental decode results of an arbitrary byte
+// sequence.
+
+#include <cstdint>
+#include <vector>
+
+#include "avif/avif.h"
+#include "avif_fuzztest_helpers.h"
+#include "avifincrtest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+using ::fuzztest::Arbitrary;
+
+namespace libavif {
+namespace testutil {
+namespace {
+
+::testing::Environment* const stack_limit_env =
+    ::testing::AddGlobalTestEnvironment(
+        new FuzztestStackLimitEnvironment("524288"));  // 512 * 1024
+
+//------------------------------------------------------------------------------
+
+struct DecoderInput {
+  const uint8_t* available_bytes;
+  size_t available_size;
+  size_t read_size;
+};
+
+// A custom reader is necessary to get the number of bytes read by libavif.
+// See avifIOReadFunc() documentation.
+avifResult AvifIoRead(struct avifIO* io, uint32_t read_flags, uint64_t offset,
+                      size_t size, avifROData* out) {
+  DecoderInput* data = reinterpret_cast<DecoderInput*>(io->data);
+  if (read_flags != 0 || !data || data->available_size < offset) {
+    return AVIF_RESULT_IO_ERROR;
+  }
+  out->data = data->available_bytes + offset;
+  out->size = std::min(size, data->available_size - offset);
+  data->read_size = std::max(data->read_size, offset + out->size);
+  return AVIF_RESULT_OK;
+}
+
+//------------------------------------------------------------------------------
+
+void DecodeIncr(const std::vector<uint8_t>& arbitrary_bytes, bool is_persistent,
+                bool give_size_hint, bool use_nth_image_api) {
+  AvifImagePtr reference(avifImageCreateEmpty(), avifImageDestroy);
+  ASSERT_NE(reference.get(), nullptr);
+
+  DecoderInput data = {arbitrary_bytes.data(), arbitrary_bytes.size(), 0};
+  avifIO io = {.read = AvifIoRead,
+               .sizeHint = arbitrary_bytes.size(),
+               .persistent = AVIF_TRUE,
+               .data = &data};
+
+  AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+  ASSERT_NE(decoder.get(), nullptr);
+  avifDecoderSetIO(decoder.get(), &io);
+
+  if (avifDecoderRead(decoder.get(), reference.get()) == AVIF_RESULT_OK) {
+    // Avoid timeouts by discarding big images decoded many times.
+    // TODO(yguyon): Increase this arbitrary threshold but decode incrementally
+    //               fewer times than as many bytes.
+    if (reference->width * reference->height * data.read_size >
+        8 * 1024 * 1024) {
+      return;
+    }
+    // decodeIncrementally() will fail if there are leftover bytes.
+    const avifRWData encoded_data = {const_cast<uint8_t*>(data.available_bytes),
+                                     data.read_size};
+    // No clue on whether encoded_data is tiled so use a lower bound of a single
+    // tile for the whole image.
+    // Note that an AVIF tile is at most as high as an AV1 frame
+    // (aomediacodec.github.io/av1-spec says max_frame_height_minus_1 < 65536)
+    // but libavif successfully decodes AVIF files with dimensions unrelated to
+    // the underlying AV1 frame (for example a 1x1000000 AVIF for a 1x1 AV1).
+    // Otherwise we could use the minimum of reference->height and 65536u below.
+    const uint32_t max_cell_height = reference->height;
+    DecodeIncrementally(encoded_data, decoder.get(), is_persistent,
+                        give_size_hint, use_nth_image_api, *reference,
+                        max_cell_height);
+  }
+}
+
+FUZZ_TEST(DecodeAvifTest, DecodeIncr)
+    .WithDomains(Arbitrary<std::vector<uint8_t>>(), Arbitrary<bool>(),
+                 Arbitrary<bool>(), Arbitrary<bool>())
+    .WithSeeds({{GetWhiteSinglePixelAvif(), false, false, false}});
+
+//------------------------------------------------------------------------------
+
+}  // namespace
+}  // namespace testutil
+}  // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec.cc b/tests/gtest/avif_fuzztest_enc_dec.cc
new file mode 100644
index 0000000..013046c
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_enc_dec.cc
@@ -0,0 +1,59 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <vector>
+
+#include "avif/avif.h"
+#include "avif_fuzztest_helpers.h"
+#include "aviftest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace testutil {
+namespace {
+
+::testing::Environment* const stack_limit_env =
+    ::testing::AddGlobalTestEnvironment(
+        new FuzztestStackLimitEnvironment("524288"));  // 512 * 1024
+
+void EncodeDecodeValid(AvifImagePtr image, AvifEncoderPtr encoder,
+                       AvifDecoderPtr decoder) {
+  AvifImagePtr decoded_image(avifImageCreateEmpty(), avifImageDestroy);
+  ASSERT_NE(image.get(), nullptr);
+  ASSERT_NE(encoder.get(), nullptr);
+  ASSERT_NE(decoder.get(), nullptr);
+  ASSERT_NE(decoded_image.get(), nullptr);
+
+  AvifRwData encoded_data;
+  const avifResult encoder_result =
+      avifEncoderWrite(encoder.get(), image.get(), &encoded_data);
+  ASSERT_EQ(encoder_result, AVIF_RESULT_OK)
+      << avifResultToString(encoder_result);
+
+  const avifResult decoder_result = avifDecoderReadMemory(
+      decoder.get(), decoded_image.get(), encoded_data.data, encoded_data.size);
+  ASSERT_EQ(decoder_result, AVIF_RESULT_OK)
+      << avifResultToString(decoder_result);
+
+  EXPECT_EQ(decoded_image->width, image->width);
+  EXPECT_EQ(decoded_image->height, image->height);
+  EXPECT_EQ(decoded_image->depth, image->depth);
+  EXPECT_EQ(decoded_image->yuvFormat, image->yuvFormat);
+
+  // Verify that an opaque input leads to an opaque output.
+  if (IsOpaque(image.get())) {
+    EXPECT_TRUE(IsOpaque(decoded_image.get()));
+  }
+  // A transparent image may be heavily compressed to an opaque image. This is
+  // hard to verify so do not check it.
+}
+
+FUZZ_TEST(EncodeDecodeAvifTest, EncodeDecodeValid)
+    .WithDomains(ArbitraryAvifImage(), ArbitraryAvifEncoder(),
+                 ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO,
+                                       AVIF_CODEC_CHOICE_DAV1D}));
+
+}  // namespace
+}  // namespace testutil
+}  // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec_experimental.cc b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
new file mode 100644
index 0000000..aac68fc
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
@@ -0,0 +1,127 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <array>
+#include <cstdint>
+#include <cstring>
+#include <memory>
+#include <vector>
+
+#include "avif/avif.h"
+#include "avif_fuzztest_helpers.h"
+#include "aviftest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace testutil {
+namespace {
+
+::testing::Environment* const stack_limit_env =
+    ::testing::AddGlobalTestEnvironment(
+        new FuzztestStackLimitEnvironment("524288"));  // 512 * 1024
+
+void CheckGainMapMetadataMatches(const avifGainMapMetadata& actual,
+                                 const avifGainMapMetadata& expected) {
+  // 'expecteed' is the source struct which has arbitrary data and booleans
+  // values can contain any value, but the decoded ('actual') struct should
+  // be 0 or 1.
+  EXPECT_EQ(actual.baseRenditionIsHDR, expected.baseRenditionIsHDR ? 1 : 0);
+  EXPECT_EQ(actual.hdrCapacityMinN, expected.hdrCapacityMinN);
+  EXPECT_EQ(actual.hdrCapacityMinD, expected.hdrCapacityMinD);
+  EXPECT_EQ(actual.hdrCapacityMaxN, expected.hdrCapacityMaxN);
+  EXPECT_EQ(actual.hdrCapacityMaxD, expected.hdrCapacityMaxD);
+  for (int c = 0; c < 3; ++c) {
+    SCOPED_TRACE(c);
+    EXPECT_EQ(actual.offsetSdrN[c], expected.offsetSdrN[c]);
+    EXPECT_EQ(actual.offsetSdrD[c], expected.offsetSdrD[c]);
+    EXPECT_EQ(actual.offsetHdrN[c], expected.offsetHdrN[c]);
+    EXPECT_EQ(actual.offsetHdrD[c], expected.offsetHdrD[c]);
+    EXPECT_EQ(actual.gainMapGammaN[c], expected.gainMapGammaN[c]);
+    EXPECT_EQ(actual.gainMapGammaD[c], expected.gainMapGammaD[c]);
+    EXPECT_EQ(actual.gainMapMinN[c], expected.gainMapMinN[c]);
+    EXPECT_EQ(actual.gainMapMinD[c], expected.gainMapMinD[c]);
+    EXPECT_EQ(actual.gainMapMaxN[c], expected.gainMapMaxN[c]);
+    EXPECT_EQ(actual.gainMapMaxD[c], expected.gainMapMaxD[c]);
+  }
+}
+
+void EncodeDecodeValid(AvifImagePtr image, AvifEncoderPtr encoder,
+                       AvifDecoderPtr decoder) {
+  AvifImagePtr decoded_image(avifImageCreateEmpty(), avifImageDestroy);
+  ASSERT_NE(image.get(), nullptr);
+  ASSERT_NE(encoder.get(), nullptr);
+  ASSERT_NE(decoder.get(), nullptr);
+  ASSERT_NE(decoded_image.get(), nullptr);
+
+  // TODO(maryla): fuzz with different settings.
+  decoder->enableDecodingGainMap = true;
+  decoder->enableParsingGainMapMetadata = true;
+
+  AvifRwData encoded_data;
+  const avifResult encoder_result =
+      avifEncoderWrite(encoder.get(), image.get(), &encoded_data);
+  ASSERT_EQ(encoder_result, AVIF_RESULT_OK)
+      << avifResultToString(encoder_result);
+
+  const avifResult decoder_result = avifDecoderReadMemory(
+      decoder.get(), decoded_image.get(), encoded_data.data, encoded_data.size);
+  ASSERT_EQ(decoder_result, AVIF_RESULT_OK)
+      << avifResultToString(decoder_result);
+
+  EXPECT_EQ(decoded_image->width, image->width);
+  EXPECT_EQ(decoded_image->height, image->height);
+  EXPECT_EQ(decoded_image->depth, image->depth);
+  EXPECT_EQ(decoded_image->yuvFormat, image->yuvFormat);
+
+  EXPECT_EQ(image->gainMap.image != nullptr,
+            decoded_image->gainMap.image != nullptr);
+  if (image->gainMap.image != nullptr) {
+    EXPECT_EQ(decoded_image->gainMap.image->width, image->gainMap.image->width);
+    EXPECT_EQ(decoded_image->gainMap.image->height,
+              image->gainMap.image->height);
+    EXPECT_EQ(decoded_image->gainMap.image->depth, image->gainMap.image->depth);
+    EXPECT_EQ(decoded_image->gainMap.image->yuvFormat,
+              image->gainMap.image->yuvFormat);
+    EXPECT_EQ(image->gainMap.image->gainMap.image, nullptr);
+    EXPECT_EQ(decoded_image->gainMap.image->alphaPlane, nullptr);
+
+    CheckGainMapMetadataMatches(decoded_image->gainMap.metadata,
+                                image->gainMap.metadata);
+  }
+
+  // Verify that an opaque input leads to an opaque output.
+  if (IsOpaque(image.get())) {
+    EXPECT_TRUE(IsOpaque(decoded_image.get()));
+  }
+  // A transparent image may be heavily compressed to an opaque image. This is
+  // hard to verify so do not check it.
+}
+
+// Note that the images are passed as raw pointers because unique_ptr didn't
+// seem to work. Similarly, avifGainMapMetadata is passed as a byte array
+// because the C array fields in the struct seem to prevent fuzztest from
+// handling it natively.
+AvifImagePtr AddGainMapToImage(
+    avifImage* image, avifImage* gainMap,
+    const std::array<uint8_t, sizeof(avifGainMapMetadata)>& metadata) {
+  image->gainMap.image = gainMap;
+  std::memcpy(&image->gainMap.metadata, metadata.data(), metadata.size());
+  return AvifImagePtr(image, avifImageDestroy);
+}
+
+inline auto ArbitraryAvifImageWithGainMap() {
+  return fuzztest::Map(
+      AddGainMapToImage, ArbitraryAvifImageRawPtr(), ArbitraryAvifImageRawPtr(),
+      fuzztest::Arbitrary<std::array<uint8_t, sizeof(avifGainMapMetadata)>>());
+}
+
+FUZZ_TEST(EncodeDecodeAvifTest8b, EncodeDecodeValid)
+    .WithDomains(fuzztest::OneOf(ArbitraryAvifImage(),
+                                 ArbitraryAvifImageWithGainMap()),
+                 ArbitraryAvifEncoder(),
+                 ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO}));
+
+}  // namespace
+}  // namespace testutil
+}  // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec_incr.cc b/tests/gtest/avif_fuzztest_enc_dec_incr.cc
new file mode 100644
index 0000000..c5f9499
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_enc_dec_incr.cc
@@ -0,0 +1,132 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+// Encode a fuzzed image split into a grid and decode it incrementally.
+// Compare the output with a non-incremental decode.
+
+#include <cassert>
+#include <cstdint>
+#include <vector>
+
+#include "avif/internal.h"
+#include "avif_fuzztest_helpers.h"
+#include "avifincrtest_helpers.h"
+#include "aviftest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+using ::fuzztest::Arbitrary;
+using ::fuzztest::InRange;
+
+namespace libavif {
+namespace testutil {
+namespace {
+
+::testing::Environment* const stack_limit_env =
+    ::testing::AddGlobalTestEnvironment(
+        new FuzztestStackLimitEnvironment("524288"));  // 512 * 1024
+
+// Splits the input image into grid_cols*grid_rows views to be encoded as a
+// grid. Returns an empty vector if the input image cannot be split that way.
+std::vector<AvifImagePtr> ImageToGrid(const AvifImagePtr& 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<AvifImagePtr> 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(), avifImageDestroy);
+      if (avifImageSetViewRect(cells.back().get(), image.get(), &rect) !=
+          AVIF_RESULT_OK) {
+        return {};
+      }
+    }
+  }
+  return cells;
+}
+
+// Converts a unique_ptr array to a raw pointer array as needed by libavif API.
+std::vector<const avifImage*> UniquePtrToRawPtr(
+    const std::vector<AvifImagePtr>& unique_ptrs) {
+  std::vector<const avifImage*> rawPtrs;
+  rawPtrs.reserve(unique_ptrs.size());
+  for (const AvifImagePtr& unique_ptr : unique_ptrs) {
+    rawPtrs.emplace_back(unique_ptr.get());
+  }
+  return rawPtrs;
+}
+
+// Encodes an image into an AVIF grid then decodes it.
+void EncodeDecodeGridValid(AvifImagePtr image, AvifEncoderPtr encoder,
+                           AvifDecoderPtr decoder, uint32_t grid_cols,
+                           uint32_t grid_rows, bool is_encoded_data_persistent,
+                           bool give_size_hint_to_decoder) {
+  ASSERT_NE(image.get(), nullptr);
+  ASSERT_NE(encoder.get(), nullptr);
+
+  std::vector<AvifImagePtr> cells = ImageToGrid(image, grid_cols, grid_rows);
+  if (cells.empty()) return;
+  const uint32_t cell_width = cells.front().get()->width;
+  const uint32_t cell_height = cells.front().get()->height;
+  const uint32_t encoded_width = std::min(image->width, grid_cols * cell_width);
+  const uint32_t encoded_height =
+      std::min(image->height, grid_rows * cell_height);
+
+  AvifRwData encoded_data;
+  const avifResult encoder_result = avifEncoderAddImageGrid(
+      encoder.get(), grid_cols, grid_rows, UniquePtrToRawPtr(cells).data(),
+      AVIF_ADD_IMAGE_FLAG_SINGLE);
+  if (((grid_cols > 1 || grid_rows > 1) &&
+       !avifAreGridDimensionsValid(image->yuvFormat, encoded_width,
+                                   encoded_height, cell_width, cell_height,
+                                   nullptr))) {
+    ASSERT_TRUE(encoder_result == AVIF_RESULT_INVALID_IMAGE_GRID)
+        << avifResultToString(encoder_result);
+    return;
+  }
+  ASSERT_EQ(encoder_result, AVIF_RESULT_OK)
+      << avifResultToString(encoder_result);
+
+  const avifResult finish_result =
+      avifEncoderFinish(encoder.get(), &encoded_data);
+  ASSERT_EQ(finish_result, AVIF_RESULT_OK) << avifResultToString(finish_result);
+
+  DecodeNonIncrementallyAndIncrementally(encoded_data, decoder.get(),
+                                         is_encoded_data_persistent,
+                                         give_size_hint_to_decoder,
+                                         /*useNthImageApi=*/true, cell_height);
+}
+
+FUZZ_TEST(EncodeDecodeAvifTest, EncodeDecodeGridValid)
+    .WithDomains(ArbitraryAvifImage(), ArbitraryAvifEncoder(),
+                 ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO}),
+                 InRange<uint32_t>(1, 32), InRange<uint32_t>(1, 32),
+                 Arbitrary<bool>(), Arbitrary<bool>());
+
+}  // namespace
+}  // namespace testutil
+}  // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_helpers.cc b/tests/gtest/avif_fuzztest_helpers.cc
new file mode 100644
index 0000000..bbd0d93
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_helpers.cc
@@ -0,0 +1,204 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include "avif_fuzztest_helpers.h"
+
+#include <algorithm>
+#include <cassert>
+
+#include "avif/avif.h"
+
+namespace libavif {
+namespace testutil {
+namespace {
+
+//------------------------------------------------------------------------------
+
+AvifImagePtr CreateAvifImage(size_t width, size_t height, int depth,
+                             avifPixelFormat pixel_format, bool has_alpha,
+                             const uint8_t* samples) {
+  AvifImagePtr image(avifImageCreate(width, height, depth, pixel_format),
+                     avifImageDestroy);
+  if (image.get() == nullptr) {
+    return image;
+  }
+  avifImageAllocatePlanes(image.get(),
+                          has_alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV);
+
+  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
+
+avifImage* CreateAvifImage8bRawPtr(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())
+      .release();
+}
+
+avifImage* CreateAvifImage16bRawPtr(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()))
+      .release();
+}
+
+AvifEncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads,
+                                 int min_quantizer, int max_quantizer,
+                                 int min_quantizer_alpha,
+                                 int max_quantizer_alpha, int tile_rows_log2,
+                                 int tile_cols_log2, int speed) {
+  AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+  if (encoder.get() == nullptr) {
+    return encoder;
+  }
+  encoder->codecChoice = codec_choice;
+  encoder->maxThreads = max_threads;
+  // minQuantizer must be at most maxQuantizer.
+  encoder->minQuantizer = std::min(min_quantizer, max_quantizer);
+  encoder->maxQuantizer = std::max(min_quantizer, max_quantizer);
+  encoder->minQuantizerAlpha =
+      std::min(min_quantizer_alpha, max_quantizer_alpha);
+  encoder->maxQuantizerAlpha =
+      std::max(min_quantizer_alpha, max_quantizer_alpha);
+  encoder->tileRowsLog2 = tile_rows_log2;
+  encoder->tileColsLog2 = tile_cols_log2;
+  encoder->speed = speed;
+  return encoder;
+}
+
+AvifDecoderPtr 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) {
+  AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+  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;
+}
+
+AvifImagePtr AvifImageToUniquePtr(avifImage* image) {
+  return AvifImagePtr(image, avifImageDestroy);
+}
+
+//------------------------------------------------------------------------------
+
+size_t GetNumSamples(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_luma_samples + num_chroma_samples + num_alpha_samples;
+}
+
+//------------------------------------------------------------------------------
+
+std::vector<uint8_t> GetWhiteSinglePixelAvif() {
+  return {
+      0x0,  0x0,  0x0,  0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66,
+      0x0,  0x0,  0x0,  0x0,  0x61, 0x76, 0x69, 0x66, 0x6d, 0x69, 0x66, 0x31,
+      0x6d, 0x69, 0x61, 0x66, 0x4d, 0x41, 0x31, 0x41, 0x0,  0x0,  0x0,  0xf2,
+      0x6d, 0x65, 0x74, 0x61, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x28,
+      0x68, 0x64, 0x6c, 0x72, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,
+      0x70, 0x69, 0x63, 0x74, 0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,
+      0x0,  0x0,  0x0,  0x0,  0x6c, 0x69, 0x62, 0x61, 0x76, 0x69, 0x66, 0x0,
+      0x0,  0x0,  0x0,  0xe,  0x70, 0x69, 0x74, 0x6d, 0x0,  0x0,  0x0,  0x0,
+      0x0,  0x1,  0x0,  0x0,  0x0,  0x1e, 0x69, 0x6c, 0x6f, 0x63, 0x0,  0x0,
+      0x0,  0x0,  0x44, 0x0,  0x0,  0x1,  0x0,  0x1,  0x0,  0x0,  0x0,  0x1,
+      0x0,  0x0,  0x1,  0x1a, 0x0,  0x0,  0x0,  0x17, 0x0,  0x0,  0x0,  0x28,
+      0x69, 0x69, 0x6e, 0x66, 0x0,  0x0,  0x0,  0x0,  0x0,  0x1,  0x0,  0x0,
+      0x0,  0x1a, 0x69, 0x6e, 0x66, 0x65, 0x2,  0x0,  0x0,  0x0,  0x0,  0x1,
+      0x0,  0x0,  0x61, 0x76, 0x30, 0x31, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x0,
+      0x0,  0x0,  0x0,  0x6a, 0x69, 0x70, 0x72, 0x70, 0x0,  0x0,  0x0,  0x4b,
+      0x69, 0x70, 0x63, 0x6f, 0x0,  0x0,  0x0,  0x14, 0x69, 0x73, 0x70, 0x65,
+      0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x0,  0x1,  0x0,  0x0,  0x0,  0x1,
+      0x0,  0x0,  0x0,  0x10, 0x70, 0x69, 0x78, 0x69, 0x0,  0x0,  0x0,  0x0,
+      0x3,  0x8,  0x8,  0x8,  0x0,  0x0,  0x0,  0xc,  0x61, 0x76, 0x31, 0x43,
+      0x81, 0x20, 0x0,  0x0,  0x0,  0x0,  0x0,  0x13, 0x63, 0x6f, 0x6c, 0x72,
+      0x6e, 0x63, 0x6c, 0x78, 0x0,  0x1,  0x0,  0xd,  0x0,  0x6,  0x80, 0x0,
+      0x0,  0x0,  0x17, 0x69, 0x70, 0x6d, 0x61, 0x0,  0x0,  0x0,  0x0,  0x0,
+      0x0,  0x0,  0x1,  0x0,  0x1,  0x4,  0x1,  0x2,  0x83, 0x4,  0x0,  0x0,
+      0x0,  0x1f, 0x6d, 0x64, 0x61, 0x74, 0x12, 0x0,  0xa,  0x7,  0x38, 0x0,
+      0x6,  0x10, 0x10, 0xd0, 0x69, 0x32, 0xa,  0x1f, 0xf0, 0x3f, 0xff, 0xff,
+      0xc4, 0x0,  0x0,  0xaf, 0x70};
+}
+
+//------------------------------------------------------------------------------
+
+template <typename SampleType>
+bool IsOpaque(const avifImage* image) {
+  if (!image->alphaPlane) {
+    return true;
+  }
+
+  const SampleType opaque_alpha = (1 << image->depth) - 1;
+  for (uint32_t j = 0; j < image->height; ++j) {
+    const SampleType* const row = reinterpret_cast<SampleType*>(
+        image->alphaPlane + j * image->alphaRowBytes);
+    for (uint32_t i = 0; i < image->width; ++i) {
+      if (row[i] != opaque_alpha) {
+        assert(row[i] < opaque_alpha);
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+bool IsOpaque(const avifImage* image) {
+  return avifImageUsesU16(image) ? IsOpaque<uint16_t>(image)
+                                 : IsOpaque<uint8_t>(image);
+}
+
+//------------------------------------------------------------------------------
+
+}  // namespace testutil
+}  // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h
new file mode 100644
index 0000000..933d23f
--- /dev/null
+++ b/tests/gtest/avif_fuzztest_helpers.h
@@ -0,0 +1,191 @@
+// Copyright 2022 Google LLC
+// SPDX-License-Identifier: BSD-2-Clause
+
+#ifndef LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_
+#define LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_
+
+#include <cstdlib>
+#include <memory>
+#include <vector>
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "fuzztest/fuzztest.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace testutil {
+
+//------------------------------------------------------------------------------
+// C++ wrapper for scoped memory management of C API objects.
+
+// Exposed for convenient fuzztest reproducer output.
+avifImage* CreateAvifImage8bRawPtr(size_t width, size_t height,
+                                   avifPixelFormat pixel_format, bool has_alpha,
+                                   const std::vector<uint8_t>& samples);
+avifImage* CreateAvifImage16bRawPtr(size_t width, size_t height, int depth,
+                                    avifPixelFormat pixel_format,
+                                    bool has_alpha,
+                                    const std::vector<uint16_t>& samples);
+AvifEncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads,
+                                 int min_quantizer, int max_quantizer,
+                                 int min_quantizer_alpha,
+                                 int max_quantizer_alpha, int tile_rows_log2,
+                                 int tile_cols_log2, int speed);
+AvifDecoderPtr 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);
+AvifImagePtr AvifImageToUniquePtr(avifImage* image);
+
+//------------------------------------------------------------------------------
+// Custom fuzztest generators.
+// See https://github.com/google/fuzztest/blob/main/doc/domains-reference.md.
+
+// Do not generate images wider or taller than this.
+inline constexpr size_t kMaxDimension = 512;  // In pixels.
+
+size_t GetNumSamples(size_t width, size_t height, avifPixelFormat pixel_format,
+                     bool has_alpha);
+
+// To avoid using fuzztest::internal, the return type of the functions below is
+// auto.
+
+inline auto ArbitraryPixelFormat() {
+  return fuzztest::ElementOf<avifPixelFormat>(
+      {AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422,
+       AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400});
+}
+
+// avifImage generator type: Width, height, pixel format and 8-bit samples.
+inline auto ArbitraryAvifImage8bRawPtr() {
+  return fuzztest::FlatMap(
+      [](size_t width, size_t height, avifPixelFormat pixel_format,
+         bool has_alpha) {
+        return fuzztest::Map(
+            CreateAvifImage8bRawPtr, fuzztest::Just(width),
+            fuzztest::Just(height), fuzztest::Just(pixel_format),
+            fuzztest::Just(has_alpha),
+            fuzztest::Arbitrary<std::vector<uint8_t>>().WithSize(
+                GetNumSamples(width, height, pixel_format, has_alpha)));
+      },
+      fuzztest::InRange<uint16_t>(1, kMaxDimension),
+      fuzztest::InRange<uint16_t>(1, kMaxDimension), ArbitraryPixelFormat(),
+      fuzztest::Arbitrary<bool>());
+}
+
+// avifImage generator type: Width, height, depth, pixel format and 16-bit
+// samples.
+inline auto ArbitraryAvifImage16bRawPtr() {
+  return fuzztest::FlatMap(
+      [](size_t width, size_t height, int depth, avifPixelFormat pixel_format,
+         bool has_alpha) {
+        return fuzztest::Map(
+            CreateAvifImage16bRawPtr, fuzztest::Just(width),
+            fuzztest::Just(height), fuzztest::Just(depth),
+            fuzztest::Just(pixel_format), fuzztest::Just(has_alpha),
+            fuzztest::ContainerOf<std::vector<uint16_t>>(
+                fuzztest::InRange<uint16_t>(0, (1 << depth) - 1))
+                .WithSize(
+                    GetNumSamples(width, height, pixel_format, has_alpha)));
+      },
+      fuzztest::InRange<uint16_t>(1, kMaxDimension),
+      fuzztest::InRange<uint16_t>(1, kMaxDimension),
+      fuzztest::ElementOf({10, 12}), ArbitraryPixelFormat(),
+      fuzztest::Arbitrary<bool>());
+}
+
+// Generator for an arbitrary avifImage* (raw pointer).
+inline auto ArbitraryAvifImageRawPtr() {
+  return fuzztest::OneOf(ArbitraryAvifImage8bRawPtr(),
+                         ArbitraryAvifImage16bRawPtr());
+}
+
+// Generator for an arbitrary AvifImagePtr (unique pointer).
+inline auto ArbitraryAvifImage() {
+  return fuzztest::Map(AvifImageToUniquePtr, ArbitraryAvifImageRawPtr());
+}
+
+// avifEncoder and avifDecoder generators
+inline auto ArbitraryAvifEncoder() {
+  const auto codec_choice = fuzztest::ElementOf<avifCodecChoice>(
+      {AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_CHOICE_AOM});
+  // MAX_NUM_THREADS from libaom/aom_util/aom_thread.h
+  const auto max_threads = fuzztest::InRange(0, 64);
+  const auto min_quantizer = fuzztest::InRange(AVIF_QUANTIZER_BEST_QUALITY,
+                                               AVIF_QUANTIZER_WORST_QUALITY);
+  const auto max_quantizer = fuzztest::InRange(AVIF_QUANTIZER_BEST_QUALITY,
+                                               AVIF_QUANTIZER_WORST_QUALITY);
+  const auto min_quantizer_alpha = fuzztest::InRange(
+      AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY);
+  const auto max_quantizer_alpha = fuzztest::InRange(
+      AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY);
+  const auto tile_rows_log2 = fuzztest::InRange(0, 6);
+  const auto tile_cols_log2 = fuzztest::InRange(0, 6);
+  // Fuzz only a small range of 'speed' values to avoid slowing down the fuzzer
+  // too much. The main goal is to fuzz libavif, not the underlying AV1 encoder.
+  const auto speed = fuzztest::InRange(6, AVIF_SPEED_FASTEST);
+  return fuzztest::Map(CreateAvifEncoder, codec_choice, max_threads,
+                       min_quantizer, max_quantizer, min_quantizer_alpha,
+                       max_quantizer_alpha, tile_rows_log2, tile_cols_log2,
+                       speed);
+}
+
+inline auto ArbitraryAvifDecoder(
+    const std::vector<avifCodecChoice>& codecChoices) {
+  const auto codec_choice = fuzztest::ElementOf<avifCodecChoice>(codecChoices);
+  // MAX_NUM_THREADS from libaom/aom_util/aom_thread.h
+  const auto max_threads = fuzztest::InRange(0, 64);
+  return fuzztest::Map(
+      CreateAvifDecoder, codec_choice, max_threads,
+      /*requested_source=*/
+      fuzztest::ElementOf(
+          {AVIF_DECODER_SOURCE_AUTO, AVIF_DECODER_SOURCE_PRIMARY_ITEM}),
+      /*allow_progressive=*/fuzztest::Arbitrary<bool>(),
+      /*allow_incremental=*/fuzztest::Arbitrary<bool>(),
+      /*ignore_exif=*/fuzztest::Arbitrary<bool>(),
+      /*ignore_xmp=*/fuzztest::Arbitrary<bool>(),
+      /*image_size_limit=*/fuzztest::Just(kMaxDimension * kMaxDimension),
+      /*image_dimension_limit=*/fuzztest::Just(kMaxDimension),
+      /*image_count_limit=*/fuzztest::Just(10),
+      /*strict_flags=*/
+      fuzztest::BitFlagCombinationOf({AVIF_STRICT_PIXI_REQUIRED,
+                                      AVIF_STRICT_CLAP_VALID,
+                                      AVIF_STRICT_ALPHA_ISPE_REQUIRED}));
+}
+
+//------------------------------------------------------------------------------
+
+// Returns a white pixel compressed with AVIF.
+std::vector<uint8_t> GetWhiteSinglePixelAvif();
+
+// Sets the FUZZTEST_STACK_LIMIT environment variable to the value passed to
+// the constructor.
+class FuzztestStackLimitEnvironment : public ::testing::Environment {
+ public:
+  FuzztestStackLimitEnvironment(const char* stack_limit)
+      : stack_limit_(stack_limit) {}
+  ~FuzztestStackLimitEnvironment() override {}
+
+  void SetUp() override { setenv("FUZZTEST_STACK_LIMIT", stack_limit_, 1); }
+
+ private:
+  const char* stack_limit_;
+};
+
+//------------------------------------------------------------------------------
+
+// Returns true if the given image has a fully opaque alpha plane, or no alpha
+// plane at all.
+bool IsOpaque(const avifImage* image);
+
+//------------------------------------------------------------------------------
+
+}  // namespace testutil
+}  // namespace libavif
+
+#endif  // LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_