Repository: python-pillow/Pillow

Issue:
Image.open() fails with struct.error with Photoshop .psd files
<!--
Thank you for reporting an issue.

Follow these guidelines to ensure your issue is handled properly.

If you have a ...

1.  General question: consider asking the question on Stack Overflow
    with the python-imaging-library tag:

    * https://stackoverflow.com/questions/tagged/python-imaging-library

    Do not ask a question in both places.

    If you think you have found a bug or have an unexplained exception
    then file a bug report here.

2.  Bug report: include a self-contained, copy-pastable example that
    generates the issue if possible. Be concise with code posted.
    Guidelines on how to provide a good bug report:

    * https://stackoverflow.com/help/mcve

    Bug reports which follow these guidelines are easier to diagnose,
    and are often handled much more quickly.

3.  Feature request: do a quick search of existing issues
    to make sure this has not been asked before.

We know asking good questions takes effort, and we appreciate your time.
Thank you.
-->

### What did you do?

Trying to process a bunch of Photoshop images primarily to obtain their size, mode, and resolution. These files open fine natively without warnings or errors.

Sample file process:

```python
from PIL import ImageFile, Image

ImageFile.LOAD_TRUNCATED_IMAGES = True

with Image.open("/Users/kp/dev/pil_psd_test/struct_error_file.psd") as img:
    print(img.format, img.size, img.mode)
```

### What did you expect to happen?

The file to open and report the format, size, and mode

### What actually happened?

```python
Traceback (most recent call last):
  File "/Users/kp/dev/sandbox/pil_psd_test.py", line 48, in <module>
    with Image.open("/Users/kp/dev/pil_psd_test/struct_error_file.psd") as img:
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/Image.py", line 3309, in open
    raise UnidentifiedImageError(msg)
PIL.UnidentifiedImageError: cannot identify image file '/Users/kp/dev/pil_psd_test/struct_error_file.psd'
```

Turning on the debug log message [here](https://github.com/python-pillow/Pillow/blob/main/src/PIL/Image.py#L3284), I can see the **actual** error is:

```python
...
DEBUG:PIL.Image:
Traceback (most recent call last):
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/ImageFile.py", line 137, in __init__
    self._open()
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/PsdImagePlugin.py", line 132, in _open
    self.layers = _layerinfo(_layer_data, size)
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/PsdImagePlugin.py", line 249, in _layerinfo
    t = _maketile(fp, m, bbox, 1)
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/PsdImagePlugin.py", line 261, in _maketile
    compression = i16(read(2))
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/_binary.py", line 81, in i16be
    return unpack_from(">H", c, o)[0]
struct.error: unpack_from requires a buffer of at least 2 bytes for unpacking 2 bytes at offset 0 (actual buffer size is 0)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/Image.py", line 3276, in _open_core
    im = factory(fp, filename)
  File "/Users/kp/dev/sandbox/.venv/lib/python3.9/site-packages/PIL/ImageFile.py", line 145, in __init__
    raise SyntaxError(v) from v
SyntaxError: unpack_from requires a buffer of at least 2 bytes for unpacking 2 bytes at offset 0 (actual buffer size is 0)
...
```

Sample image is here:
[struct_error_file.psd.zip](https://github.com/python-pillow/Pillow/files/13875177/struct_error_file.psd.zip)


### What are your OS, Python and Pillow versions?

* OS: MacOS 14.2.1 (23C71)
* Python: 3.9.17
* Pillow: 10.2.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

```python
see above
```



Discussion:

Resolving patch:
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -24,6 +24,7 @@
 from ._binary import i16be as i16
 from ._binary import i32be as i32
 from ._binary import si16be as si16
+from ._binary import si32be as si32
 
 MODES = {
     # (photoshop mode, bits) -> (pil mode, required channels)
@@ -177,22 +178,21 @@ def read(size):
 
     for _ in range(abs(ct)):
         # bounding box
-        y0 = i32(read(4))
-        x0 = i32(read(4))
-        y1 = i32(read(4))
-        x1 = i32(read(4))
+        y0 = si32(read(4))
+        x0 = si32(read(4))
+        y1 = si32(read(4))
+        x1 = si32(read(4))
 
         # image info
         mode = []
         ct_types = i16(read(2))
-        types = list(range(ct_types))
-        if len(types) > 4:
-            fp.seek(len(types) * 6 + 12, io.SEEK_CUR)
+        if ct_types > 4:
+            fp.seek(ct_types * 6 + 12, io.SEEK_CUR)
             size = i32(read(4))
             fp.seek(size, io.SEEK_CUR)
             continue
 
-        for _ in types:
+        for _ in range(ct_types):
             type = i16(read(2))
 
             if type == 65535:
diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -77,6 +77,16 @@ def si32le(c: bytes, o: int = 0) -> int:
     return unpack_from("<i", c, o)[0]
 
 
+def si32be(c: bytes, o: int = 0) -> int:
+    """
+    Converts a 4-bytes (32 bits) string to a signed integer, big endian.
+
+    :param c: string containing bytes to convert
+    :param o: offset of bytes to convert in string
+    """
+    return unpack_from(">i", c, o)[0]
+
+
 def i16be(c: bytes, o: int = 0) -> int:
     return unpack_from(">H", c, o)[0]
 


Test patch:
diff --git a/Tests/images/negative_top_left_layer.psd b/Tests/images/negative_top_left_layer.psd
new file mode 100644
Binary files /dev/null and b/Tests/images/negative_top_left_layer.psd differ
diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py
--- a/Tests/test_file_psd.py
+++ b/Tests/test_file_psd.py
@@ -113,6 +113,11 @@ def test_rgba() -> None:
         assert_image_equal_tofile(im, "Tests/images/imagedraw_square.png")
 
 
+def test_negative_top_left_layer() -> None:
+    with Image.open("Tests/images/negative_top_left_layer.psd") as im:
+        assert im.layers[0][2] == (-50, -50, 50, 50)
+
+
 def test_layer_skip() -> None:
     with Image.open("Tests/images/five_channels.psd") as im:
         assert im.n_frames == 1