Repository: python-pillow/Pillow

Issue:
Pillow fails/hangs on PSD hidden background layer
Depending on file content, Pillow:

- fails with "cannot identify image file";
- fails with "Truncated File Read";
- just hangs inside the `PsdImagePlugin._layerinfo` function because it thinks there are 65534 layers.

See attachment: [pillow-psd-fail.zip](https://github.com/python-pillow/Pillow/files/6815914/pillow-psd-fail.zip)

GIMP works fine with these files.


Discussion:
To provide some further information -

- pillow-cannot-identify.psd is hitting https://github.com/python-pillow/Pillow/blob/696b82e44032500197734c4dc4669d7c3f8b9371/src/PIL/PsdImagePlugin.py#L186
- pillow-truncated-file-read.psd is hitting https://github.com/python-pillow/Pillow/blob/696b82e44032500197734c4dc4669d7c3f8b9371/src/PIL/ImageFile.py#L562
- pillow-hangs.psd does not loop forever. It may take a minute, but it does finish
Ok, I've realised that the problem is that we're not allowing for the layer count to be negative.

https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1031423
> Layer count. If it is a negative number, its absolute value is the number of layers and the first alpha channel contains the transparency data for the merged result.

Are we able to include pillow-cannot-identify.psd in our test suite and distribute it under the Pillow license?
>Are we able to include pillow-cannot-identify.psd in our test suite and distribute it under the Pillow license?

Yes (if it doesn't contain any sensitive meta information) — it's completely empty and I think it is not copyrighted :)

(But pillow-hangs.psd contains a t-shirt image from an unknown source, so I can't relicense it)

Techically, you can recreate these files from scratch using the [Layer > New > Background from Layer](https://helpx.adobe.com/photoshop/key-concepts/background.html) menu and then hiding the background layer
I don't actually have Photoshop. If you want to recreate an image with the same problem and attach it, I can use that instead.
These files are already recreated, so everything is fine I think

Resolving patch:
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -22,6 +22,7 @@
 from ._binary import i8
 from ._binary import i16be as i16
 from ._binary import i32be as i32
+from ._binary import si16be as si16
 
 MODES = {
     # (photoshop mode, bits) -> (pil mode, required channels)
@@ -179,7 +180,7 @@ def _layerinfo(fp, ct_bytes):
     def read(size):
         return ImageFile._safe_read(fp, size)
 
-    ct = i16(read(2))
+    ct = si16(read(2))
 
     # sanity check
     if ct_bytes < (abs(ct) * 20):
diff --git a/src/PIL/_binary.py b/src/PIL/_binary.py
--- a/src/PIL/_binary.py
+++ b/src/PIL/_binary.py
@@ -47,6 +47,16 @@ def si16le(c, o=0):
     return unpack_from("<h", c, o)[0]
 
 
+def si16be(c, o=0):
+    """
+    Converts a 2-bytes (16 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(">h", c, o)[0]
+
+
 def i32le(c, o=0):
     """
     Converts a 4-bytes (32 bits) string to an unsigned integer.


Test patch:
diff --git a/Tests/images/negative_layer_count.psd b/Tests/images/negative_layer_count.psd
new file mode 100644
Binary files /dev/null and b/Tests/images/negative_layer_count.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
@@ -57,9 +57,10 @@ def test_n_frames():
         assert im.n_frames == 1
         assert not im.is_animated
 
-    with Image.open(test_file) as im:
-        assert im.n_frames == 2
-        assert im.is_animated
+    for path in [test_file, "Tests/images/negative_layer_count.psd"]:
+        with Image.open(path) as im:
+            assert im.n_frames == 2
+            assert im.is_animated
 
 
 def test_eoferror():