1#
2# The Python Imaging Library.
3# $Id$
4#
5# IPTC/NAA file handling
6#
7# history:
8# 1995-10-01 fl Created
9# 1998-03-09 fl Cleaned up and added to PIL
10# 2002-06-18 fl Added getiptcinfo helper
11#
12# Copyright (c) Secret Labs AB 1997-2002.
13# Copyright (c) Fredrik Lundh 1995.
14#
15# See the README file for information on usage and redistribution.
16#
17from __future__ import annotations
18
19from io import BytesIO
20from typing import cast
21
22from . import Image, ImageFile
23from ._binary import i16be as i16
24from ._binary import i32be as i32
25
26COMPRESSION = {1: "raw", 5: "jpeg"}
27
28
29#
30# Helpers
31
32
33def _i(c: bytes) -> int:
34 return i32((b"\0\0\0\0" + c)[-4:])
35
36
37def _i8(c: int | bytes) -> int:
38 return c if isinstance(c, int) else c[0]
39
40
41##
42# Image plugin for IPTC/NAA datastreams. To read IPTC/NAA fields
43# from TIFF and JPEG files, use the <b>getiptcinfo</b> function.
44
45
46class IptcImageFile(ImageFile.ImageFile):
47 format = "IPTC"
48 format_description = "IPTC/NAA"
49
50 def getint(self, key: tuple[int, int]) -> int:
51 return _i(self.info[key])
52
53 def field(self) -> tuple[tuple[int, int] | None, int]:
54 #
55 # get a IPTC field header
56 s = self.fp.read(5)
57 if not s.strip(b"\x00"):
58 return None, 0
59
60 tag = s[1], s[2]
61
62 # syntax
63 if s[0] != 0x1C or tag[0] not in [1, 2, 3, 4, 5, 6, 7, 8, 9, 240]:
64 msg = "invalid IPTC/NAA file"
65 raise SyntaxError(msg)
66
67 # field size
68 size = s[3]
69 if size > 132:
70 msg = "illegal field length in IPTC/NAA file"
71 raise OSError(msg)
72 elif size == 128:
73 size = 0
74 elif size > 128:
75 size = _i(self.fp.read(size - 128))
76 else:
77 size = i16(s, 3)
78
79 return tag, size
80
81 def _open(self) -> None:
82 # load descriptive fields
83 while True:
84 offset = self.fp.tell()
85 tag, size = self.field()
86 if not tag or tag == (8, 10):
87 break
88 if size:
89 tagdata = self.fp.read(size)
90 else:
91 tagdata = None
92 if tag in self.info:
93 if isinstance(self.info[tag], list):
94 self.info[tag].append(tagdata)
95 else:
96 self.info[tag] = [self.info[tag], tagdata]
97 else:
98 self.info[tag] = tagdata
99
100 # mode
101 layers = self.info[(3, 60)][0]
102 component = self.info[(3, 60)][1]
103 if (3, 65) in self.info:
104 id = self.info[(3, 65)][0] - 1
105 else:
106 id = 0
107 if layers == 1 and not component:
108 self._mode = "L"
109 elif layers == 3 and component:
110 self._mode = "RGB"[id]
111 elif layers == 4 and component:
112 self._mode = "CMYK"[id]
113
114 # size
115 self._size = self.getint((3, 20)), self.getint((3, 30))
116
117 # compression
118 try:
119 compression = COMPRESSION[self.getint((3, 120))]
120 except KeyError as e:
121 msg = "Unknown IPTC image compression"
122 raise OSError(msg) from e
123
124 # tile
125 if tag == (8, 10):
126 self.tile = [
127 ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
128 ]
129
130 def load(self) -> Image.core.PixelAccess | None:
131 if len(self.tile) != 1 or self.tile[0][0] != "iptc":
132 return ImageFile.ImageFile.load(self)
133
134 offset, compression = self.tile[0][2:]
135
136 self.fp.seek(offset)
137
138 # Copy image data to temporary file
139 o = BytesIO()
140 if compression == "raw":
141 # To simplify access to the extracted file,
142 # prepend a PPM header
143 o.write(b"P5\n%d %d\n255\n" % self.size)
144 while True:
145 type, size = self.field()
146 if type != (8, 10):
147 break
148 while size > 0:
149 s = self.fp.read(min(size, 8192))
150 if not s:
151 break
152 o.write(s)
153 size -= len(s)
154
155 with Image.open(o) as _im:
156 _im.load()
157 self.im = _im.im
158 self.tile = []
159 return Image.Image.load(self)
160
161
162Image.register_open(IptcImageFile.format, IptcImageFile)
163
164Image.register_extension(IptcImageFile.format, ".iim")
165
166
167def getiptcinfo(
168 im: ImageFile.ImageFile,
169) -> dict[tuple[int, int], bytes | list[bytes]] | None:
170 """
171 Get IPTC information from TIFF, JPEG, or IPTC file.
172
173 :param im: An image containing IPTC data.
174 :returns: A dictionary containing IPTC information, or None if
175 no IPTC information block was found.
176 """
177 from . import JpegImagePlugin, TiffImagePlugin
178
179 data = None
180
181 info: dict[tuple[int, int], bytes | list[bytes]] = {}
182 if isinstance(im, IptcImageFile):
183 # return info dictionary right away
184 for k, v in im.info.items():
185 if isinstance(k, tuple):
186 info[k] = v
187 return info
188
189 elif isinstance(im, JpegImagePlugin.JpegImageFile):
190 # extract the IPTC/NAA resource
191 photoshop = im.info.get("photoshop")
192 if photoshop:
193 data = photoshop.get(0x0404)
194
195 elif isinstance(im, TiffImagePlugin.TiffImageFile):
196 # get raw data from the IPTC/NAA tag (PhotoShop tags the data
197 # as 4-byte integers, so we cannot use the get method...)
198 try:
199 data = im.tag_v2._tagdata[TiffImagePlugin.IPTC_NAA_CHUNK]
200 except KeyError:
201 pass
202
203 if data is None:
204 return None # no properties
205
206 # create an IptcImagePlugin object without initializing it
207 class FakeImage:
208 pass
209
210 fake_im = FakeImage()
211 fake_im.__class__ = IptcImageFile # type: ignore[assignment]
212 iptc_im = cast(IptcImageFile, fake_im)
213
214 # parse the IPTC information chunk
215 iptc_im.info = {}
216 iptc_im.fp = BytesIO(data)
217
218 try:
219 iptc_im._open()
220 except (IndexError, KeyError):
221 pass # expected failure
222
223 for k, v in iptc_im.info.items():
224 if isinstance(k, tuple):
225 info[k] = v
226 return info