Add avifImageCopySamples() to internal.h

Remove testutil::CopyImageSamples().
Use avifImageCopySamples() in read.c.
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 54cf85f..4eb5bf2 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -89,6 +89,11 @@
 // Copies all fields that do not need to be freed/allocated from srcImage to dstImage.
 void avifImageCopyNoAlloc(avifImage * dstImage, const avifImage * srcImage);
 
+// Copies the samples from srcImage to dstImage. dstImage must be allocated.
+// srcImage and dstImage must have the same width, height, and depth.
+// If the AVIF_PLANES_YUV bit is set in planes, then srcImage and dstImage must have the same yuvFormat and yuvRange.
+void avifImageCopySamples(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes);
+
 typedef struct avifAlphaParams
 {
     uint32_t width;
diff --git a/src/avif.c b/src/avif.c
index 087c827..8fa2b36 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -182,6 +182,44 @@
     dstImage->imir = srcImage->imir;
 }
 
+void avifImageCopySamples(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes)
+{
+    assert(srcImage->depth == dstImage->depth);
+    if (planes & AVIF_PLANES_YUV) {
+        assert((srcImage->yuvFormat == dstImage->yuvFormat) && (srcImage->yuvRange == dstImage->yuvRange));
+    }
+    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;
+        }
+    }
+}
+
 avifResult avifImageCopy(avifImage * dstImage, const avifImage * srcImage, avifPlanesFlags planes)
 {
     avifImageFreePlanes(dstImage, AVIF_PLANES_ALL);
@@ -208,22 +246,7 @@
             return allocationResult;
         }
     }
-    for (int plane = AVIF_CHAN_Y; plane <= AVIF_CHAN_A; ++plane) {
-        uint8_t * dstRow = avifImagePlane(dstImage, plane);
-        if (!dstRow) {
-            continue;
-        }
-        const uint8_t * srcRow = avifImagePlane(srcImage, plane);
-        uint32_t srcRowBytes = avifImagePlaneRowBytes(srcImage, plane);
-        uint32_t dstRowBytes = avifImagePlaneRowBytes(dstImage, plane);
-        uint32_t planeWidthBytes = avifImagePlaneWidth(dstImage, plane) << (dstImage->depth > 8);
-        uint32_t planeHeight = avifImagePlaneHeight(dstImage, plane);
-        for (uint32_t j = 0; j < planeHeight; ++j) {
-            memcpy(dstRow, srcRow, planeWidthBytes);
-            srcRow += srcRowBytes;
-            dstRow += dstRowBytes;
-        }
-    }
+    avifImageCopySamples(dstImage, srcImage, planes);
     return AVIF_RESULT_OK;
 }
 
diff --git a/src/read.c b/src/read.c
index 14f9041..d115e23 100644
--- a/src/read.c
+++ b/src/read.c
@@ -1375,11 +1375,14 @@
     avifGetPixelFormatInfo(firstTile->image->yuvFormat, &formatInfo);
 
     unsigned int tileIndex = oldDecodedTileCount;
-    size_t pixelBytes = avifImageUsesU16(dstImage) ? 2 : 1;
     unsigned int rowIndex = oldDecodedTileCount / grid->columns;
     unsigned int colIndex = oldDecodedTileCount % grid->columns;
     // Only the first iteration of the outer for loop uses this initial value of colIndex.
     // Subsequent iterations of the outer for loop initializes colIndex to 0.
+    avifImage srcView;
+    avifImageSetDefaults(&srcView);
+    avifImage dstView;
+    avifImageSetDefaults(&dstView);
     for (; rowIndex < grid->rows; ++rowIndex, colIndex = 0) {
         for (; colIndex < grid->columns; ++colIndex, ++tileIndex) {
             if (tileIndex >= decodedTileCount) {
@@ -1388,60 +1391,22 @@
             }
             avifTile * tile = &data->tiles.tile[firstTileIndex + tileIndex];
 
-            unsigned int widthToCopy = firstTile->image->width;
-            unsigned int maxX = firstTile->image->width * (colIndex + 1);
-            if (maxX > grid->outputWidth) {
-                widthToCopy -= maxX - grid->outputWidth;
+            avifCropRect dstViewRect = {
+                firstTile->image->width * colIndex, firstTile->image->height * rowIndex, firstTile->image->width, firstTile->image->height
+            };
+            if (dstViewRect.x + dstViewRect.width > grid->outputWidth) {
+                dstViewRect.width = grid->outputWidth - dstViewRect.x;
             }
-
-            unsigned int heightToCopy = firstTile->image->height;
-            unsigned int maxY = firstTile->image->height * (rowIndex + 1);
-            if (maxY > grid->outputHeight) {
-                heightToCopy -= maxY - grid->outputHeight;
+            if (dstViewRect.y + dstViewRect.height > grid->outputHeight) {
+                dstViewRect.height = grid->outputHeight - dstViewRect.y;
             }
-
-            // Y and A channels
-            size_t yaColOffset = (size_t)colIndex * firstTile->image->width;
-            size_t yaRowOffset = (size_t)rowIndex * firstTile->image->height;
-            size_t yaRowBytes = widthToCopy * pixelBytes;
-
-            if (alpha) {
-                // A
-                for (unsigned int j = 0; j < heightToCopy; ++j) {
-                    const uint8_t * src = &tile->image->alphaPlane[j * tile->image->alphaRowBytes];
-                    uint8_t * dst = &dstImage->alphaPlane[(yaColOffset * pixelBytes) + ((yaRowOffset + j) * dstImage->alphaRowBytes)];
-                    memcpy(dst, src, yaRowBytes);
-                }
-            } else {
-                // Y
-                for (unsigned int j = 0; j < heightToCopy; ++j) {
-                    const uint8_t * src = &tile->image->yuvPlanes[AVIF_CHAN_Y][j * tile->image->yuvRowBytes[AVIF_CHAN_Y]];
-                    uint8_t * dst =
-                        &dstImage->yuvPlanes[AVIF_CHAN_Y][(yaColOffset * pixelBytes) + ((yaRowOffset + j) * dstImage->yuvRowBytes[AVIF_CHAN_Y])];
-                    memcpy(dst, src, yaRowBytes);
-                }
-
-                if (!firstTileUVPresent) {
-                    continue;
-                }
-
-                // UV
-                heightToCopy >>= formatInfo.chromaShiftY;
-                size_t uvColOffset = yaColOffset >> formatInfo.chromaShiftX;
-                size_t uvRowOffset = yaRowOffset >> formatInfo.chromaShiftY;
-                size_t uvRowBytes = yaRowBytes >> formatInfo.chromaShiftX;
-                for (unsigned int j = 0; j < heightToCopy; ++j) {
-                    const uint8_t * srcU = &tile->image->yuvPlanes[AVIF_CHAN_U][j * tile->image->yuvRowBytes[AVIF_CHAN_U]];
-                    uint8_t * dstU =
-                        &dstImage->yuvPlanes[AVIF_CHAN_U][(uvColOffset * pixelBytes) + ((uvRowOffset + j) * dstImage->yuvRowBytes[AVIF_CHAN_U])];
-                    memcpy(dstU, srcU, uvRowBytes);
-
-                    const uint8_t * srcV = &tile->image->yuvPlanes[AVIF_CHAN_V][j * tile->image->yuvRowBytes[AVIF_CHAN_V]];
-                    uint8_t * dstV =
-                        &dstImage->yuvPlanes[AVIF_CHAN_V][(uvColOffset * pixelBytes) + ((uvRowOffset + j) * dstImage->yuvRowBytes[AVIF_CHAN_V])];
-                    memcpy(dstV, srcV, uvRowBytes);
-                }
+            const avifCropRect srcViewRect = { 0, 0, dstViewRect.width, dstViewRect.height };
+            if ((avifImageSetViewRect(&dstView, dstImage, &dstViewRect) != AVIF_RESULT_OK) ||
+                (avifImageSetViewRect(&srcView, tile->image, &srcViewRect) != AVIF_RESULT_OK)) {
+                assert(AVIF_FALSE);
+                return AVIF_FALSE;
             }
+            avifImageCopySamples(&dstView, &srcView, alpha ? AVIF_PLANES_A : AVIF_PLANES_YUV);
         }
     }
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 4cdd8f3..57639d7 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -91,7 +91,7 @@
     add_test(NAME avifcllitest COMMAND avifcllitest)
 
     add_executable(avifgridapitest gtest/avifgridapitest.cc)
-    target_link_libraries(avifgridapitest aviftest_helpers ${GTEST_BOTH_LIBRARIES})
+    target_link_libraries(avifgridapitest avif_internal aviftest_helpers ${GTEST_BOTH_LIBRARIES})
     target_include_directories(avifgridapitest PRIVATE ${GTEST_INCLUDE_DIRS})
     add_test(NAME avifgridapitest COMMAND avifgridapitest)
 
diff --git a/tests/gtest/avifgridapitest.cc b/tests/gtest/avifgridapitest.cc
index 6c606d9..96fe7fc 100644
--- a/tests/gtest/avifgridapitest.cc
+++ b/tests/gtest/avifgridapitest.cc
@@ -4,6 +4,7 @@
 #include <vector>
 
 #include "avif/avif.h"
+#include "avif/internal.h"
 #include "aviftest_helpers.h"
 #include "gtest/gtest.h"
 
@@ -94,7 +95,7 @@
       if (result != AVIF_RESULT_OK) {
         return result;
       }
-      testutil::CopyImageSamples(**it, view.get());
+      avifImageCopySamples(/*dstImage=*/view.get(), it->get(), AVIF_PLANES_ALL);
       assert(!view->imageOwnsYUVPlanes);
       ++it;
       rect.x += rect.width;
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index ed05d2a..283a681 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -220,37 +220,6 @@
   return true;
 }
 
-void CopyImageSamples(const avifImage& from, avifImage* to) {
-  assert(from.width == to->width);
-  assert(from.height == to->height);
-  assert(from.depth == to->depth);
-  assert(from.yuvFormat == to->yuvFormat);
-  assert(from.yuvRange == to->yuvRange);
-
-  for (avifChannelIndex c :
-       {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) {
-    const uint8_t* from_row = avifImagePlane(&from, c);
-    uint8_t* to_row = avifImagePlane(to, c);
-    assert(!from_row == !to_row);
-    const uint32_t from_row_bytes = avifImagePlaneRowBytes(&from, c);
-    const uint32_t to_row_bytes = avifImagePlaneRowBytes(to, c);
-    const uint32_t plane_width = avifImagePlaneWidth(&from, c);
-    // 0 for A if no alpha and 0 for UV if 4:0:0.
-    const uint32_t plane_height = avifImagePlaneHeight(&from, c);
-    for (uint32_t y = 0; y < plane_height; ++y) {
-      if (avifImageUsesU16(&from)) {
-        std::copy(reinterpret_cast<const uint16_t*>(from_row),
-                  reinterpret_cast<const uint16_t*>(from_row) + plane_width,
-                  reinterpret_cast<uint16_t*>(to_row));
-      } else {
-        std::copy(from_row, from_row + plane_width, to_row);
-      }
-      from_row += from_row_bytes;
-      to_row += to_row_bytes;
-    }
-  }
-}
-
 //------------------------------------------------------------------------------
 
 AvifImagePtr ReadImage(const char* folder_path, const char* file_name,
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index 420d1d2..b2674e7 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -50,10 +50,6 @@
                          avifPixelFormat yuv_format, avifPlanesFlags planes,
                          avifRange yuv_range = AVIF_RANGE_FULL);
 
-// Copy the pixel values from an image to another. They must share the same
-// features (dimensions, depth etc.).
-void CopyImageSamples(const avifImage& from, avifImage* to);
-
 // Set all pixels of each plane of an image.
 void FillImagePlain(avifImage* image, const uint32_t yuva[4]);
 void FillImageGradient(avifImage* image);