Fix direct modifications of decoder->image. (#3281)

Directly modifying it goes against the API contract.
Create views instead.
diff --git a/apps/avifdec.c b/apps/avifdec.c
index a9d23ae..f5e6567 100644
--- a/apps/avifdec.c
+++ b/apps/avifdec.c
@@ -304,6 +304,7 @@
 
     // ------ After this point, use 'goto cleanup;' in case of failure ------
     int returnCode = 1;
+    avifImage * imageView = NULL;
     avifDecoder * decoder = avifDecoderCreate();
     if (!decoder) {
         fprintf(stderr, "Memory allocation failure\n");
@@ -366,6 +367,10 @@
     const avifBool decodeAllFrames = frameIndex == DECODE_ALL_FRAMES;
     int currIndex = decodeAllFrames ? 0 : frameIndex;
     for (;;) {
+        if (imageView != NULL && imageView != decoder->image) {
+            avifImageDestroy(imageView);
+        }
+        imageView = NULL;
         result = decodeAllFrames ? avifDecoderNextImage(decoder) : avifDecoderNthImage(decoder, frameIndex);
         if (result != AVIF_RESULT_OK) {
             break;
@@ -400,20 +405,34 @@
             }
         }
 
-        if (ignoreICC && (decoder->image->icc.size > 0)) {
-            printf("[--ignore-icc] Discarding ICC profile.\n");
-            // This cannot fail.
-            result = avifImageSetProfileICC(decoder->image, NULL, 0);
-            assert(result == AVIF_RESULT_OK);
-        }
-
-        if (iccOverrideFilename) {
-            printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename);
-            result = avifImageSetProfileICC(decoder->image, iccOverride.data, iccOverride.size);
-            if (result != AVIF_RESULT_OK) {
-                fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result));
+        if (ignoreICC) {
+            imageView = avifImageCreateEmpty();
+            if (!imageView) {
+                fprintf(stderr, "ERROR: Out of memory\n");
                 goto cleanup;
             }
+            if (decoder->image->icc.size > 0) {
+                printf("[--ignore-icc] Discarding ICC profile.\n");
+            }
+            result = avifImageCreateView(imageView,
+                                         decoder->image,
+                                         /*ignoreColorProfile=*/AVIF_TRUE,
+                                         /*ignoreAlpha=*/AVIF_FALSE,
+                                         /*ignoreGainMap=*/AVIF_FALSE);
+            if (result != AVIF_RESULT_OK) {
+                fprintf(stderr, "ERROR: Failed to create image view\n");
+                goto cleanup;
+            }
+            if (iccOverrideFilename) {
+                printf("[--icc] Setting ICC profile: %s\n", iccOverrideFilename);
+                result = avifImageSetProfileICC(imageView, iccOverride.data, iccOverride.size);
+                if (result != AVIF_RESULT_OK) {
+                    fprintf(stderr, "ERROR: Failed to set ICC: %s\n", avifResultToString(result));
+                    goto cleanup;
+                }
+            }
+        } else {
+            imageView = decoder->image;
         }
 
         if (decodeAllFrames) {
@@ -436,11 +455,11 @@
                 fprintf(stderr, "ERROR: Unable to generate output filename\n");
                 goto cleanup;
             }
-            if (!avifWriteToFile(outputFormat, frameFilename, decoder->image, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) {
+            if (!avifWriteToFile(outputFormat, frameFilename, imageView, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) {
                 goto cleanup;
             }
         } else {
-            if (!avifWriteToFile(outputFormat, outputFilename, decoder->image, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) {
+            if (!avifWriteToFile(outputFormat, outputFilename, imageView, rawColor, jpegQuality, pngCompressionLevel, requestedDepth, chromaUpsampling)) {
                 goto cleanup;
             }
             if (isSequence && !frameIndexSpecified) {
@@ -476,6 +495,9 @@
         if (returnCode != 0) {
             avifDumpDiagnostics(&decoder->diag);
         }
+        if (imageView != NULL && imageView != decoder->image) {
+            avifImageDestroy(imageView);
+        }
         avifDecoderDestroy(decoder);
     }
     avifRWDataFree(&iccOverride);
diff --git a/apps/avifgainmaputil/extractgainmap_command.cc b/apps/avifgainmaputil/extractgainmap_command.cc
index 5779ab8..b926296 100644
--- a/apps/avifgainmaputil/extractgainmap_command.cc
+++ b/apps/avifgainmaputil/extractgainmap_command.cc
@@ -4,6 +4,7 @@
 #include "extractgainmap_command.h"
 
 #include "avif/avif_cxx.h"
+#include "avifutil.h"
 #include "imageio.h"
 
 namespace avif {
@@ -25,21 +26,30 @@
   decoder->maxThreads = arg_jobs_.jobs.value();
   decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_GAIN_MAP;
 
-  avifResult result =
-      ReadAvif(decoder.get(), arg_input_filename_, /*ignore_profile=*/true,
-               /*ignore_alpha=*/false);
+  avifResult result = ReadAvif(decoder.get(), arg_input_filename_);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
 
-  if (decoder->image->gainMap == nullptr ||
-      decoder->image->gainMap->image == nullptr) {
+  ImagePtr image(avifImageCreateEmpty());
+  if (!image) {
+    return AVIF_RESULT_OUT_OF_MEMORY;
+  }
+  result = avifImageCreateView(image.get(), decoder->image,
+                               /*ignoreColorProfile=*/true,
+                               /*ignoreAlpha=*/false,
+                               /*ignoreGainMap=*/false);
+  if (result != AVIF_RESULT_OK) {
+    return result;
+  }
+
+  if (image->gainMap == nullptr || image->gainMap->image == nullptr) {
     std::cerr << "Input image " << arg_input_filename_
               << " does not contain a gain map\n";
     return AVIF_RESULT_INVALID_ARGUMENT;
   }
 
-  return WriteImage(decoder->image->gainMap->image,
+  return WriteImage(image->gainMap->image,
                     arg_image_encode_.grid.value().grid_cols,
                     arg_image_encode_.grid.value().grid_rows,
                     arg_output_filename_, arg_image_encode_.quality,
diff --git a/apps/avifgainmaputil/imageio.cc b/apps/avifgainmaputil/imageio.cc
index 7786301..7aae803 100644
--- a/apps/avifgainmaputil/imageio.cc
+++ b/apps/avifgainmaputil/imageio.cc
@@ -205,29 +205,31 @@
       decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
     }
     decoder->maxThreads = jobs;
-    avifResult result =
-        ReadAvif(decoder.get(), input_filename, ignore_profile, ignore_alpha);
+    avifResult result = ReadAvif(decoder.get(), input_filename);
     if (result != AVIF_RESULT_OK) {
       return result;
     }
-    if (ignore_gain_map && decoder->image->gainMap) {
-      avifGainMapDestroy(decoder->image->gainMap);
-      decoder->image->gainMap = nullptr;
+
+    ImagePtr view(avifImageCreateEmpty());
+    if (!view) {
+      return AVIF_RESULT_OUT_OF_MEMORY;
     }
+    result = avifImageCreateView(view.get(), decoder->image, ignore_profile,
+                                 ignore_alpha, ignore_gain_map);
+    if (result != AVIF_RESULT_OK) {
+      return result;
+    }
+
     const avifColorPrimaries in_primaries = image->colorPrimaries;
     const avifTransferCharacteristics in_transfer =
         image->transferCharacteristics;
     const avifMatrixCoefficients in_matrix = image->matrixCoefficients;
-    if (decoder->image->imageOwnsYUVPlanes &&
-        (decoder->image->alphaPlane == nullptr ||
-         decoder->image->imageOwnsAlphaPlane)) {
-      std::swap(*image, *decoder->image);
-    } else {
-      result = avifImageCopy(image, decoder->image, AVIF_PLANES_ALL);
-      if (result != AVIF_RESULT_OK) {
-        return result;
-      }
+
+    result = avifImageCopy(image, view.get(), AVIF_PLANES_ALL);
+    if (result != AVIF_RESULT_OK) {
+      return result;
     }
+
     if (in_primaries != AVIF_COLOR_PRIMARIES_UNSPECIFIED ||
         in_transfer != AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED ||
         in_matrix != AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) {
@@ -264,8 +266,7 @@
   return AVIF_RESULT_OK;
 }
 
-avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename,
-                    bool ignore_profile, bool ignore_alpha) {
+avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename) {
   avifResult result = avifDecoderSetIOFile(decoder, input_filename.c_str());
   if (result != AVIF_RESULT_OK) {
     std::cerr << "Cannot open file for read: " << input_filename << "\n";
@@ -283,15 +284,6 @@
               << " (" << decoder->diag.error << ")\n";
     return result;
   }
-  if (ignore_profile) {
-    avifRWDataFree(&decoder->image->icc);
-    if (decoder->image->gainMap) {
-      avifRWDataFree(&decoder->image->gainMap->altICC);
-    }
-  }
-  if (ignore_alpha && decoder->image->alphaPlane) {
-    avifImageFreePlanes(decoder->image, AVIF_PLANES_A);
-  }
 
   return AVIF_RESULT_OK;
 }
diff --git a/apps/avifgainmaputil/imageio.h b/apps/avifgainmaputil/imageio.h
index 308bd2a..48d0275 100644
--- a/apps/avifgainmaputil/imageio.h
+++ b/apps/avifgainmaputil/imageio.h
@@ -33,8 +33,7 @@
 
 // Reads an image in avif format given a pre-configured decoder.
 // The image can be accessed at decoder->image.
-avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename,
-                    bool ignore_profile, bool ignore_alpha);
+avifResult ReadAvif(avifDecoder* decoder, const std::string& input_filename);
 
 }  // namespace avif
 
diff --git a/apps/avifgainmaputil/swapbase_command.cc b/apps/avifgainmaputil/swapbase_command.cc
index d46c904..38a5a7f 100644
--- a/apps/avifgainmaputil/swapbase_command.cc
+++ b/apps/avifgainmaputil/swapbase_command.cc
@@ -4,6 +4,7 @@
 #include "swapbase_command.h"
 
 #include "avif/avif_cxx.h"
+#include "avifutil.h"
 #include "imageio.h"
 
 namespace avif {
@@ -164,14 +165,23 @@
   }
   decoder->maxThreads = arg_jobs_.jobs.value();
   decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
-  avifResult result =
-      ReadAvif(decoder.get(), arg_input_filename_,
-               arg_image_read_.ignore_profile, arg_image_read_.ignore_alpha);
+  avifResult result = ReadAvif(decoder.get(), arg_input_filename_);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
 
-  avifImage* const image = decoder->image;
+  ImagePtr image(avifImageCreateEmpty());
+  if (!image) {
+    return AVIF_RESULT_OUT_OF_MEMORY;
+  }
+  result = avifImageCreateView(image.get(), decoder->image,
+                               arg_image_read_.ignore_profile,
+                               arg_image_read_.ignore_alpha,
+                               /*ignore_gain_map=*/false);
+  if (result != AVIF_RESULT_OK) {
+    return result;
+  }
+
   if (image->gainMap == nullptr || image->gainMap->image == nullptr) {
     std::cerr << "Input image " << arg_input_filename_
               << " does not contain a gain map\n";
diff --git a/apps/avifgainmaputil/tonemap_command.cc b/apps/avifgainmaputil/tonemap_command.cc
index 27d366f..42fe627 100644
--- a/apps/avifgainmaputil/tonemap_command.cc
+++ b/apps/avifgainmaputil/tonemap_command.cc
@@ -6,6 +6,7 @@
 #include <cmath>
 
 #include "avif/avif_cxx.h"
+#include "avifutil.h"
 #include "imageio.h"
 
 namespace avif {
@@ -66,21 +67,30 @@
   }
   decoder->maxThreads = arg_jobs_.jobs.value();
   decoder->imageContentToDecode |= AVIF_IMAGE_CONTENT_GAIN_MAP;
-  avifResult result =
-      ReadAvif(decoder.get(), arg_input_filename_,
-               arg_image_read_.ignore_profile, arg_image_read_.ignore_alpha);
+  avifResult result = ReadAvif(decoder.get(), arg_input_filename_);
   if (result != AVIF_RESULT_OK) {
     return result;
   }
-  if (arg_input_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
-    decoder->image->colorPrimaries = arg_input_cicp_.value().color_primaries;
-    decoder->image->transferCharacteristics =
-        arg_input_cicp_.value().transfer_characteristics;
-    decoder->image->matrixCoefficients =
-        arg_input_cicp_.value().matrix_coefficients;
+
+  ImagePtr image(avifImageCreateEmpty());
+  if (!image) {
+    return AVIF_RESULT_OUT_OF_MEMORY;
+  }
+  result = avifImageCreateView(image.get(), decoder->image,
+                               arg_image_read_.ignore_profile,
+                               arg_image_read_.ignore_alpha,
+                               /*ignore_gain_map=*/false);
+  if (result != AVIF_RESULT_OK) {
+    return result;
   }
 
-  avifImage* const image = decoder->image;
+  if (arg_input_cicp_.provenance() == argparse::Provenance::SPECIFIED) {
+    image->colorPrimaries = arg_input_cicp_.value().color_primaries;
+    image->transferCharacteristics =
+        arg_input_cicp_.value().transfer_characteristics;
+    image->matrixCoefficients = arg_input_cicp_.value().matrix_coefficients;
+  }
+
   if (image->gainMap == nullptr || image->gainMap->image == nullptr) {
     std::cerr << "Input image " << arg_input_filename_
               << " does not contain a gain map\n";
@@ -203,7 +213,7 @@
   }
   avifDiagnostics diag;
   result = avifImageApplyGainMap(
-      image, image->gainMap, arg_headroom_, cicp.color_primaries,
+      image.get(), image->gainMap, arg_headroom_, cicp.color_primaries,
       cicp.transfer_characteristics, &tone_mapped_rgb,
       clli_set ? nullptr : &clli_box, &diag);
   if (result != AVIF_RESULT_OK) {
diff --git a/apps/shared/avifutil.c b/apps/shared/avifutil.c
index cd2f6dc..2fe63e1 100644
--- a/apps/shared/avifutil.c
+++ b/apps/shared/avifutil.c
@@ -823,3 +823,94 @@
     }
     return AVIF_RESULT_OK;
 }
+
+avifResult avifImageCreateView(avifImage * dstImage, const avifImage * srcImage, avifBool ignoreColorProfile, avifBool ignoreAlpha, avifBool ignoreGainMap)
+{
+    avifResult res = AVIF_RESULT_OK;
+    if (!dstImage || !srcImage) {
+        return AVIF_RESULT_INVALID_ARGUMENT;
+    }
+
+    avifCropRect rect = { 0, 0, srcImage->width, srcImage->height };
+    res = avifImageSetViewRect(dstImage, srcImage, &rect);
+    if (res != AVIF_RESULT_OK) {
+        return res;
+    }
+
+    if (ignoreAlpha && dstImage->alphaPlane) {
+        avifImageFreePlanes(dstImage, AVIF_PLANES_A);
+    }
+
+    if (!ignoreColorProfile) {
+        res = avifImageSetProfileICC(dstImage, srcImage->icc.data, srcImage->icc.size);
+        if (res != AVIF_RESULT_OK) {
+            return res;
+        }
+    }
+
+    // Using avifRWDataSet directly to match the internal avifImageCopy() behavior.
+    // This avoids re-extracting Exif orientation into irot/imir on an already decoded image.
+    res = avifRWDataSet(&dstImage->exif, srcImage->exif.data, srcImage->exif.size);
+    if (res != AVIF_RESULT_OK) {
+        return res;
+    }
+    res = avifImageSetMetadataXMP(dstImage, srcImage->xmp.data, srcImage->xmp.size);
+    if (res != AVIF_RESULT_OK) {
+        return res;
+    }
+
+    for (size_t i = 0; i < srcImage->numProperties; ++i) {
+        if (memcmp(srcImage->properties[i].boxtype, "uuid", 4) == 0) {
+            res = avifImageAddUUIDProperty(dstImage,
+                                           srcImage->properties[i].usertype,
+                                           srcImage->properties[i].boxPayload.data,
+                                           srcImage->properties[i].boxPayload.size);
+        } else {
+            res = avifImageAddOpaqueProperty(dstImage,
+                                             srcImage->properties[i].boxtype,
+                                             srcImage->properties[i].boxPayload.data,
+                                             srcImage->properties[i].boxPayload.size);
+        }
+        if (res != AVIF_RESULT_OK) {
+            return res;
+        }
+    }
+
+    if (!ignoreGainMap && srcImage->gainMap) {
+        dstImage->gainMap = avifGainMapCreate();
+        if (!dstImage->gainMap) {
+            return AVIF_RESULT_OUT_OF_MEMORY;
+        }
+
+        // Copy all gain map scalars at once
+        *dstImage->gainMap = *srcImage->gainMap;
+        // Reset pointers to prevent shared ownership and double-free
+        dstImage->gainMap->image = NULL;
+        dstImage->gainMap->altICC.data = NULL;
+        dstImage->gainMap->altICC.size = 0;
+
+        if (!ignoreColorProfile && srcImage->gainMap->altICC.size > 0) {
+            res = avifRWDataSet(&dstImage->gainMap->altICC, srcImage->gainMap->altICC.data, srcImage->gainMap->altICC.size);
+            if (res != AVIF_RESULT_OK) {
+                return res;
+            }
+        }
+
+        if (srcImage->gainMap->image) {
+            dstImage->gainMap->image = avifImageCreateEmpty();
+            if (!dstImage->gainMap->image) {
+                return AVIF_RESULT_OUT_OF_MEMORY;
+            }
+            res = avifImageCreateView(dstImage->gainMap->image,
+                                      srcImage->gainMap->image,
+                                      ignoreColorProfile,
+                                      /*ignoreAlpha=*/AVIF_TRUE,
+                                      /*ignoreGainMap=*/AVIF_TRUE);
+            if (res != AVIF_RESULT_OK) {
+                return res;
+            }
+        }
+    }
+
+    return AVIF_RESULT_OK;
+}
diff --git a/apps/shared/avifutil.h b/apps/shared/avifutil.h
index 192bbc7..30f6996 100644
--- a/apps/shared/avifutil.h
+++ b/apps/shared/avifutil.h
@@ -128,6 +128,12 @@
 // This must match the cited fallback for "--yuv auto" in avifenc.c's syntax() function.
 #define AVIF_APP_DEFAULT_PIXEL_FORMAT AVIF_PIXEL_FORMAT_YUV444
 
+// Creates a shallow copy of an image (a view of the pixels, but deep copy of metadata).
+// If a gain map is present in 'srcImage', the gain map of 'dstImage' is also set to
+// a view of the original gain map.
+// 'dstImage' should be an empty image. It will not own the pixel data.
+avifResult avifImageCreateView(avifImage * dstImage, const avifImage * srcImage, avifBool ignoreColorProfile, avifBool ignoreAlpha, avifBool ignoreGainMap);
+
 #ifdef __cplusplus
 } // extern "C"
 #endif