Repository: python-pillow/Pillow

Issue:
Image.save() causes images created by Image.frombuffer() to stop reflecting changes in that buffer

Hi, recently messed with PIL to encounter what is probably a bug

### What did you do?

```python
import numpy as np
from PIL import Image


# Source of data for image
im_data: np.typing.NDArray = np.array([[255]], dtype=np.uint8)
assert im_data[0, 0] == 255  # OK

# Attach to buffer protocol of an array, should reflect changes in im_data
im: Image.Image = Image.frombuffer(mode="L", size=(1, 1), data=im_data)
assert im_data[0, 0] == 255  # OK
assert im.getdata()[0] == 255  # OK

im_data[:, :] = np.array([[128]], dtype=np.uint8)
assert im_data[0, 0] == 128  # OK
assert im.getdata()[0] == 128  # OK!

im.save("./test_image.bmp")
assert im_data[0, 0] == 128  # OK
assert im.getdata()[0] == 128  # OK

im_data[:, :] = np.array([[64]], dtype=np.uint8)
assert im_data[0, 0] == 64  # OK
assert im.getdata()[0] == 64  # Failed!!! It is still == 128
```

### What did you expect to happen?

Line with last assert should pass the assertion:
```
...
assert im.getdata()[0] == 64   # Should pass
```

### What actually happened?

Yes, so it seams that `Image.save()` messes with image buffer state. Moreover, this is actually explicitly stated that it does in source code:

https://github.com/python-pillow/Pillow/blob/af09976a5f57e8aebaedf332bc4025ea1b068af4/src/PIL/Image.py#L2513

```python
#     def save(...):
#         ...
           # may mutate self!
           self._ensure_mutable()

```

Once one saved the image, one can no longer work with buffer and expect image working properly.

I can *try* to work out some patches but really I am somewhat new to the pillow and not well aware of all internal connections with things. It would be nice to have some hints on what may going on - then I can try to fix this.

### What are your OS, Python and Pillow versions?

#### OS:
```shell
lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 22.04.5 LTS
Release:	22.04
Codename:	jammy
```

#### Python: 
- `Python 3.12.9`
- Prebuilt binary from deadsnakes ppa if I remember correctly

#### Pillow: 

```shell
poetry run python3 -m PIL --report
--------------------------------------------------------------------
Pillow 11.1.0
Python 3.12.9 (main, Feb  5 2025, 08:49:00) [GCC 11.4.0]
--------------------------------------------------------------------
Python executable is [...]/.venv/bin/python3
Environment Python files loaded from [...]/.venv
System Python files loaded from /usr
--------------------------------------------------------------------
Python Pillow modules loaded from [...]/.venv/lib/python3.12/site-packages/PIL
Binary Pillow modules loaded from [...]/.venv/lib/python3.12/site-packages/PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 11.1.0
*** TKINTER support not installed
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.5.0
--- JPEG support ok, compiled for libjpeg-turbo 3.1.0
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.3
--- ZLIB (PNG/ZIP) support ok, loaded 1.2.11, compiled for zlib-ng 2.2.2
--- LIBTIFF support ok, loaded 4.6.0
--- RAQM (Bidirectional Text) support ok, loaded 0.10.1, fribidi 1.0.8, harfbuzz 10.1.0
*** LIBIMAGEQUANT (Quantization method) support not installed
--- XCB (X protocol) support ok
--------------------------------------------------------------------
```


Discussion hints:

Solution patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 233df592c..c62d7a8a3 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -2540,8 +2540,13 @@ class Image:
                 msg = f"unknown file extension: {ext}"
                 raise ValueError(msg) from e
 
+        from . import ImageFile
+
         # may mutate self!
-        self._ensure_mutable()
+        if isinstance(self, ImageFile.ImageFile) and filename == self.filename:
+            self._ensure_mutable()
+        else:
+            self.load()
 
         save_all = params.pop("save_all", None)
         self.encoderinfo = {**getattr(self, "encoderinfo", {}), **params}


Test patch:
diff --git a/Tests/test_image.py b/Tests/test_image.py
index c2e850c36..7e6118d52 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -258,6 +258,15 @@ class TestImage:
             assert im.readonly
             im.save(temp_file)
 
+    def test_save_without_changing_readonly(self, tmp_path: Path) -> None:
+        temp_file = tmp_path / "temp.bmp"
+
+        with Image.open("Tests/images/rgb32bf-rgba.bmp") as im:
+            assert im.readonly
+
+            im.save(temp_file)
+            assert im.readonly
+
     def test_dump(self, tmp_path: Path) -> None:
         im = Image.new("L", (10, 10))
         im._dump(str(tmp_path / "temp_L.ppm"))