Repository: python-pillow/Pillow

Issue:
Photoshop EXIF tags are not read
Hello, Hello!

I need to detect if a file has Photoshop clipping paths present using the corresponding EXIF tags [(0x07d0 - 0xbb5)](https://exiftool.org/TagNames/Photoshop.html) However Pillow seems to ignore these tags.

```python
from PIL import Image

img=Image.open('mypsdTiff.tiff')
psd_paths = range(0x07d0, 0x0bb5)
exif_tags = list(img.getexif().keys())
print ("Yay!" if any(path in exif_tags for path in psd_paths) else "Nay!")
```

Result: "Nay!"
Expected Result: "Yay!"

The tag in the file is the 0x07d0. I did test it out with exiftool.

I have the feeling I have to add this range to PIL.TiffTags?

OS: Windows 11 (and Server 2008)
Python Version: 3.9 (and 3.10)
Pillow version: 9.01



Discussion:
Hi. Would you be able to attach a copy of mypsdTiff.tiff?
Hey radar, thanks for replying so quick. Here it is. In a ZIP file because else I couldn't upload it. 
For reference:
- exiftool can be found [here](https://github.com/exiftool/exiftool) to read the paths it needs a config file ([photoshop_paths.config](https://github.com/exiftool/exiftool/blob/master/config_files/photoshop_paths.config) which is found under the configs_files dir

[mypsdTiff.zip](https://github.com/python-pillow/Pillow/files/8009238/mypsdTiff.zip)

Thanks.

If you run `exiftool -v -v mypsdTiff.tiff`, part of the output will be
```
  | 20) PhotoshopSettings (SubDirectory) -->
  |     - Tag 0x8649 (6736 bytes, int8u[6736] read as undef[6736])
  | + [Photoshop directory, 6736 bytes]
  | | IPTCDigest = ...}.....pv....N
  | | - Tag 0x0425 (16 bytes)
  | | PrintInfo2 = ...printOutput.PstSbool.InteenumInteClrm.printSixteenBitbool.printer[snip]
  | | - Tag 0x043a (279 bytes)
  | | PrintStyle = ...printOutputOptions.CptnboolClbrboolRgsMboolCrnCboolCntCboolLblsbo[snip]
  | | - Tag 0x043b (557 bytes)
  | | ResolutionInfo (SubDirectory) -->
  | | - Tag 0x03ed (16 bytes)
  | | + [BinaryData directory, 16 bytes]
  | | | XResolution = 19660800
  | | | - Tag 0x0000 (4 bytes, int32u[1])
  | | | DisplayedUnitsX = 1
  | | | - Tag 0x0002 (2 bytes, int16u[1])
  | | | YResolution = 19660800
  | | | - Tag 0x0004 (4 bytes, int32u[1])
  | | | DisplayedUnitsY = 1
  | | | - Tag 0x0006 (2 bytes, int16u[1])
  | | PrintScaleInfo (SubDirectory) -->
  | | - Tag 0x0426 (14 bytes)
  | | + [BinaryData directory, 14 bytes]
  | | | PrintStyle = 0
  | | | - Tag 0x0000 (2 bytes, int16u[1])
  | | | PrintPosition = 0 0
  | | | - Tag 0x0002 (8 bytes, float[2])
  | | | PrintScale = 1
  | | | - Tag 0x000a (4 bytes, float[1])
  | | BackgroundColor = ......
  | | - Tag 0x03f2 (10 bytes)
  | | GlobalAngle = .
  | | - Tag 0x040d (4 bytes)
  | | GlobalAltitude = .
  | | - Tag 0x0419 (4 bytes)
  | | PrintFlags = 0 0 0 0 0 0 0 0 1
  | | - Tag 0x03f3 (9 bytes, int8u[9])
  | | PrintFlagsInfo = ..
  | | - Tag 0x2710 (10 bytes)
  | | ColorHalftoningInfo = /ff.lff../ff......2.Z..5.-..
  | | - Tag 0x03f5 (72 bytes)
  | | ColorTransferFuncs = ............................................................[snip]
  | | - Tag 0x03f8 (112 bytes)
  | | TargetLayerID = 0
  | | - Tag 0x0400 (2 bytes, int16u[1])
  | | LayersGroupInfo = 0 0 0
  | | - Tag 0x0402 (6 bytes, int16u[3])
  | | LayerGroupsEnabledID = 1 1 1
  | | - Tag 0x0430 (3 bytes, int8u[3])
  | | LayerSelectionIDs = ..
  | | - Tag 0x042d (6 bytes)
  | | GridGuidesInfo = ..@.@
  | | - Tag 0x0408 (16 bytes)
  | | URL_List = 
  | | - Tag 0x041e (4 bytes)
  | | SliceInfo (SubDirectory) -->
  | | - Tag 0x041a (833 bytes)
  | | + [BinaryData directory, 833 bytes]
  | | | SlicesGroupName = din931
  | | | - Tag 0x0014 (12 bytes, ustr32[12])
  | | | NumSlices = 1
  | | | - Tag 0x0018 (4 bytes, int32u[1])
  | | PixelInfo (SubDirectory) -->
  | | - Tag 0x0428 (12 bytes)
  | | + [BinaryData directory, 12 bytes]
  | | | PixelAspectRatio = 1
  | | | - Tag 0x0004 (8 bytes, double[1])
  | | ICC_Untagged = 1
  | | - Tag 0x0411 (1 bytes, int8u[1])
  | | IDsBaseValue = 5
  | | - Tag 0x0414 (4 bytes, int32u[1])
  | | PhotoshopThumbnail = ..^.....v.......Adobe_CM....Adobed..........................[snip]
  | | - Tag 0x040c (2962 bytes)
  | | VersionInfo (SubDirectory) -->
  | | - Tag 0x0421 (97 bytes)
  | | + [BinaryData directory, 97 bytes]
  | | | HasRealMergedData = 1
  | | | - Tag 0x0004 (1 bytes, int8u[1])
  | | | WriterName = Adobe Photoshop
  | | | - Tag 0x0005 (30 bytes, ustr32[30])
  | | | ReaderName = Adobe Photoshop CC 2015.5
  | | | - Tag 0x0009 (50 bytes, ustr32[50])
  | | Photoshop_0x07d0 = ..+...N.....DD..v....?......T.....U.../..Sy..\.T.....U....e.X.[snip]
  | | - Tag 0x07d0, Name="Pfad 1" (1196 bytes)
  | | OriginPathInfo = ..null..keyRootDescriptorListVlLs.Objc.null..keyDescriptorListVl[snip]
  | | - Tag 0x0bb8, Name="OriginDataIRB" (154 bytes)
```

The information you're after is within the PhotoshopSettings subdirectory, tag 0x8649, a ["Collection of Photoshop 'Image Resource Blocks'"](https://www.awaresystems.be/imaging/tiff/tifftags/photoshop.html).

I've created PR #6030 to add a new method, `getphotoshopblocks()`, that extracts information from tag 0x8649. With that PR, the following code
```python
from PIL import Image
img=Image.open('mypsdTiff.tiff')
psd_paths = range(0x07d0, 0x0bb5)
blocks = list(img.getphotoshopblocks().keys())
print("Yay!" if any(path in blocks for path in psd_paths) else "Nay!")
```
prints "Yay!"

Resolving patch:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -41,6 +41,7 @@
 import io
 import itertools
 import logging
+import math
 import os
 import struct
 import warnings
@@ -49,6 +50,8 @@
 from numbers import Number, Rational
 
 from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
+from ._binary import i16be as i16
+from ._binary import i32be as i32
 from ._binary import o8
 from .TiffTags import TYPES
 
@@ -1129,6 +1132,27 @@ def getxmp(self):
         """
         return self._getxmp(self.tag_v2[700]) if 700 in self.tag_v2 else {}
 
+    def get_photoshop_blocks(self):
+        """
+        Returns a dictionary of Photoshop "Image Resource Blocks".
+        The keys are the image resource ID. For more information, see
+        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727
+
+        :returns: Photoshop "Image Resource Blocks" in a dictionary.
+        """
+        blocks = {}
+        val = self.tag_v2.get(0x8649)
+        if val:
+            while val[:4] == b"8BIM":
+                id = i16(val[4:6])
+                n = math.ceil((val[6] + 1) / 2) * 2
+                size = i32(val[6 + n : 10 + n])
+                data = val[10 + n : 10 + n + size]
+                blocks[id] = {"data": data}
+
+                val = val[math.ceil((10 + n + size) / 2) * 2 :]
+        return blocks
+
     def load(self):
         if self.tile and self.use_load_libtiff:
             return self._load_libtiff()


Test patch:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -685,6 +685,32 @@ def test_getxmp(self):
                 assert description[0]["format"] == "image/tiff"
                 assert description[3]["BitsPerSample"]["Seq"]["li"] == ["8", "8", "8"]
 
+    def test_get_photoshop_blocks(self):
+        with Image.open("Tests/images/lab.tif") as im:
+            assert list(im.get_photoshop_blocks().keys()) == [
+                1061,
+                1002,
+                1005,
+                1062,
+                1037,
+                1049,
+                1011,
+                1034,
+                10000,
+                1013,
+                1016,
+                1032,
+                1054,
+                1050,
+                1064,
+                1041,
+                1044,
+                1036,
+                1057,
+                4000,
+                4001,
+            ]
+
     def test_close_on_load_exclusive(self, tmp_path):
         # similar to test_fd_leak, but runs on unixlike os
         tmpfile = str(tmp_path / "temp.tif")