Repository: python-pillow/Pillow

Problem statement:
Change in behavior with JPEG Images in 11.0.0
The following code that worked in Pillow 10.4 no longer works with Pillow 11.0. 

```python
import pickle

from io import BytesIO

from PIL import Image

im = Image.open("books.jpg")

p = pickle.dumps(im)
im = pickle.loads(p)

# Fails in 11.0, works in 10.4
output = BytesIO()
im.save(output, format=im.format, quality="keep")
```

Error stack.

```pytb
Traceback (most recent call last):
  File "test.py", line 15, in <module>
    im.save(output, format=im.format, quality="keep")
  File "python3.9/site-packages/PIL/Image.py", line 2605, in save
    save_handler(self, fp, filename)
  File "python3.9/site-packages/PIL/JpegImagePlugin.py", line 700, in _save
    subsampling = get_sampling(im)
  File "python3.9/site-packages/PIL/JpegImagePlugin.py", line 643, in get_sampling
    if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
  File "python3.9/site-packages/PIL/JpegImagePlugin.py", line 396, in __getattr__
    raise AttributeError(name)
AttributeError: layers
```

This has been traced to the following change in src/PIL/JpegImagePlugin.py

# Pillow 10.4.0
[Code](https://github.com/python-pillow/Pillow/blob/10.4.0/src/PIL/JpegImagePlugin.py#L627)
```python
def get_sampling(im):
...
    if not hasattr(im, "layers") or im.layers in (1, 4):
        return -1
...
```

# Pillow 11.0.0
[Code](https://github.com/python-pillow/Pillow/blob/11.0.0/src/PIL/JpegImagePlugin.py#L643)
```python
def get_sampling(im: Image.Image) -> int:
...
    if not isinstance(im, JpegImageFile) or im.layers in (1, 4):
        return -1
...
```

# Suggested fix
```python
def get_sampling(im: Image.Image) -> int:
...
    if not isinstance(im, JpegImageFile) or not hasattr(im, "layers") or im.layers in (1, 4):
        return -1
...
```

Note that the following hack does workaround the issue in 11.0. 

```python
im.layers = 1
im.save(output, format=im.format, quality="keep")
```



Hints:
Is this a bug or feature? It looks like Pillow got more strict in verifying that `im` was a `JpegImageFile` ?
That is a good question. Serializing an image and reloading it changes how it looks. 

I wasn't sure if this check for the `layers` attribute was at one point intentional for this use case or just coincidental. 

Is there a reason you want to pickle the `ImageFile` instead of just opening the image again?
The serialization is happening in a different part of the code. This example is for simplicity in reproducing.

Patch:
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -763,7 +763,7 @@ def __getstate__(self) -> list[Any]:
 
     def __setstate__(self, state: list[Any]) -> None:
         Image.__init__(self)
-        info, mode, size, palette, data = state
+        info, mode, size, palette, data = state[:5]
         self.info = info
         self._mode = mode
         self._size = size
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -395,6 +395,13 @@ def __getattr__(self, name: str) -> Any:
             return getattr(self, "_" + name)
         raise AttributeError(name)
 
+    def __getstate__(self) -> list[Any]:
+        return super().__getstate__() + [self.layers, self.layer]
+
+    def __setstate__(self, state: list[Any]) -> None:
+        super().__setstate__(state)
+        self.layers, self.layer = state[5:]
+
     def load_read(self, read_bytes: int) -> bytes:
         """
         internal: read more image data


Test patch:
diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py
--- a/Tests/test_pickle.py
+++ b/Tests/test_pickle.py
@@ -74,6 +74,17 @@ def test_pickle_image(
     helper_pickle_file(tmp_path, protocol, test_file, test_mode)
 
 
+def test_pickle_jpeg() -> None:
+    # Arrange
+    with Image.open("Tests/images/hopper.jpg") as image:
+        # Act: roundtrip
+        unpickled_image = pickle.loads(pickle.dumps(image))
+
+    # Assert
+    assert len(unpickled_image.layer) == 3
+    assert unpickled_image.layers == 3
+
+
 def test_pickle_la_mode_with_palette(tmp_path: Path) -> None:
     # Arrange
     filename = str(tmp_path / "temp.pkl")