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