Repository: matplotlib/matplotlib

Issue:
[Bug]: image alpha re-applied each draw?
### Bug summary

On `main`, if I plot an RGBA image and then mess about with the gui window size, the image keeps getting darker.

### Code for reproduction

```Python
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
rgba = np.random.random((2, 2, 4))

fig, ax = plt.subplots()

ax.imshow(rgba)

plt.show()
```

### Actual outcome

After resizing the window a lot

![Image](https://github.com/user-attachments/assets/0e65c995-7568-4279-94d3-260dccbdaed9)

### Expected outcome

![Image](https://github.com/user-attachments/assets/b5145c6a-039f-4522-a2d3-6a25327c9251)

### Additional information

Bisects to #29776 (cc @anntzer)

The problem is specific to RGBA - I noticed it first with the code from https://github.com/matplotlib/matplotlib/issues/26092#issuecomment-1683341255, where only the top two plots of the righthand column are affected.

### Operating system

RHEL9

### Matplotlib Version

main

### Matplotlib Backend

qtagg

### Python version

3.12.7

### Jupyter version

n/a

### Installation

git checkout


Discussion:
I _think_ it's because for RGBA we are multiplying the original array here, but I am way out of my depth...

https://github.com/matplotlib/matplotlib/blob/7e997ae87b2abd927830445df5d768c835503504/lib/matplotlib/image.py#L523
This bisects to 27cb5e31b3ab5bca86cc20080442e7798185fbd0
Which is consistent with your instict that it's that line that's caused the issue
Undoing the in-place multiplication by putting a `A[..., :3] /= A[..., 3:]` after the resample line fixes this, so I think you're definitely correct about what's causing it. I'm not sure if adding that line is the best fix or not though.
@dstansby thanks!  I have started a branch, so will put up a PR soon and we can hash it out :-)



Patch:
diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py
index bd1254c27fe1..760235b6284d 100644
--- a/lib/matplotlib/image.py
+++ b/lib/matplotlib/image.py
@@ -439,6 +439,8 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
             if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)):
                 raise ValueError(f"Invalid shape {A.shape} for image data")
 
+            float_rgba_in = A.ndim == 3 and A.shape[-1] == 4 and A.dtype.kind == 'f'
+
             # if antialiased, this needs to change as window sizes
             # change:
             interpolation_stage = self._interpolation_stage
@@ -520,6 +522,9 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
                 # Resample in premultiplied alpha space.  (TODO: Consider
                 # implementing premultiplied-space resampling in
                 # span_image_resample_rgba_affine::generate?)
+                if float_rgba_in and np.ndim(alpha) == 0 and np.any(A[..., 3] < 1):
+                    # Do not modify original RGBA input
+                    A = A.copy()
                 A[..., :3] *= A[..., 3:]
                 res = _resample(self, A, out_shape, t)
                 np.divide(res[..., :3], res[..., 3:], out=res[..., :3],


Test patch:
diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py
index 0e9f3fb37fbd..93124141487f 100644
--- a/lib/matplotlib/tests/test_image.py
+++ b/lib/matplotlib/tests/test_image.py
@@ -281,6 +281,33 @@ def test_imshow_alpha(fig_test, fig_ref):
     ax3.imshow(rgbau)
 
 
+@pytest.mark.parametrize('n_channels, is_int, alpha_arr, opaque',
+                         [(3, False, False, False),  # RGB float
+                          (4, False, False, False),  # RGBA float
+                          (4, False, True, False),   # RGBA float with alpha array
+                          (4, False, False, True),   # RGBA float with solid color
+                          (4, True, False, False)])  # RGBA unint8
+def test_imshow_multi_draw(n_channels, is_int, alpha_arr, opaque):
+    if is_int:
+        array = np.random.randint(0, 256, (2, 2, n_channels))
+    else:
+        array = np.random.random((2, 2, n_channels))
+        if opaque:
+            array[:, :, 3] = 1
+
+    if alpha_arr:
+        alpha = np.array([[0.3, 0.5], [1, 0.8]])
+    else:
+        alpha = None
+
+    fig, ax = plt.subplots()
+    im = ax.imshow(array, alpha=alpha)
+    fig.draw_without_rendering()
+
+    # Draw should not modify original array
+    np.testing.assert_array_equal(array, im._A)
+
+
 def test_cursor_data():
     from matplotlib.backend_bases import MouseEvent