codekingpro/portable-devtools
115k
1#
2# The Python Imaging Library.
3# $Id$
4#
5# TIFF file handling
6#
7# TIFF is a flexible, if somewhat aged, image file format originally
8# defined by Aldus. Although TIFF supports a wide variety of pixel
9# layouts and compression methods, the name doesn't really stand for
10# "thousands of incompatible file formats," it just feels that way.
11#
12# To read TIFF data from a stream, the stream must be seekable. For
13# progressive decoding, make sure to use TIFF files where the tag
14# directory is placed first in the file.
15#
16# History:
17# 1995-09-01 fl Created
18# 1996-05-04 fl Handle JPEGTABLES tag
19# 1996-05-18 fl Fixed COLORMAP support
20# 1997-01-05 fl Fixed PREDICTOR support
21# 1997-08-27 fl Added support for rational tags (from Perry Stoll)
22# 1998-01-10 fl Fixed seek/tell (from Jan Blom)
23# 1998-07-15 fl Use private names for internal variables
24# 1999-06-13 fl Rewritten for PIL 1.0 (1.0)
25# 2000-10-11 fl Additional fixes for Python 2.0 (1.1)
26# 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2)
27# 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3)
28# 2001-12-18 fl Added workaround for broken Matrox library
29# 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart)
30# 2003-05-19 fl Check FILLORDER tag
31# 2003-09-26 fl Added RGBa support
32# 2004-02-24 fl Added DPI support; fixed rational write support
33# 2005-02-07 fl Added workaround for broken Corel Draw 10 files
34# 2006-01-09 fl Added support for float/double tags (from Russell Nelson)
35#
36# Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved.
37# Copyright (c) 1995-1997 by Fredrik Lundh
38#
39# See the README file for information on usage and redistribution.
40#
41from __future__ import annotations
42
43import io
44import itertools
45import logging
46import math
47import os
48import struct
49import warnings
50from collections.abc import Callable, MutableMapping
51from fractions import Fraction
52from numbers import Number, Rational
53from typing import IO, Any, cast
54
55from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
56from ._binary import i16be as i16
57from ._binary import i32be as i32
58from ._binary import o8
59from ._util import DeferredError, is_path
60from .TiffTags import TYPES
61
62TYPE_CHECKING = False
63if TYPE_CHECKING:
64 from collections.abc import Iterator
65 from typing import NoReturn
66
67 from ._typing import Buffer, IntegralLike, StrOrBytesPath
68
69logger = logging.getLogger(__name__)
70
71# Set these to true to force use of libtiff for reading or writing.
72READ_LIBTIFF = False
73WRITE_LIBTIFF = False
74STRIP_SIZE = 65536
75
76II = b"II" # little-endian (Intel style)
77MM = b"MM" # big-endian (Motorola style)
78
79#
80# --------------------------------------------------------------------
81# Read TIFF files
82
83# a few tag names, just to make the code below a bit more readable
84OSUBFILETYPE = 255
85IMAGEWIDTH = 256
86IMAGELENGTH = 257
87BITSPERSAMPLE = 258
88COMPRESSION = 259
89PHOTOMETRIC_INTERPRETATION = 262
90FILLORDER = 266
91IMAGEDESCRIPTION = 270
92STRIPOFFSETS = 273
93SAMPLESPERPIXEL = 277
94ROWSPERSTRIP = 278
95STRIPBYTECOUNTS = 279
96X_RESOLUTION = 282
97Y_RESOLUTION = 283
98PLANAR_CONFIGURATION = 284
99RESOLUTION_UNIT = 296
100TRANSFERFUNCTION = 301
101SOFTWARE = 305
102DATE_TIME = 306
103ARTIST = 315
104PREDICTOR = 317
105COLORMAP = 320
106TILEWIDTH = 322
107TILELENGTH = 323
108TILEOFFSETS = 324
109TILEBYTECOUNTS = 325
110SUBIFD = 330
111EXTRASAMPLES = 338
112SAMPLEFORMAT = 339
113JPEGTABLES = 347
114YCBCRSUBSAMPLING = 530
115REFERENCEBLACKWHITE = 532
116COPYRIGHT = 33432
117IPTC_NAA_CHUNK = 33723 # newsphoto properties
118PHOTOSHOP_CHUNK = 34377 # photoshop properties
119ICCPROFILE = 34675
120EXIFIFD = 34665
121XMP = 700
122JPEGQUALITY = 65537 # pseudo-tag by libtiff
123
124# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
125IMAGEJ_META_DATA_BYTE_COUNTS = 50838
126IMAGEJ_META_DATA = 50839
127
128COMPRESSION_INFO = {
129 # Compression => pil compression name
130 1: "raw",
131 2: "tiff_ccitt",
132 3: "group3",
133 4: "group4",
134 5: "tiff_lzw",
135 6: "tiff_jpeg", # obsolete
136 7: "jpeg",
137 8: "tiff_adobe_deflate",
138 32771: "tiff_raw_16", # 16-bit padding
139 32773: "packbits",
140 32809: "tiff_thunderscan",
141 32946: "tiff_deflate",
142 34676: "tiff_sgilog",
143 34677: "tiff_sgilog24",
144 34925: "lzma",
145 50000: "zstd",
146 50001: "webp",
147}
148
149COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
150
151OPEN_INFO = {
152 # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
153 # ExtraSamples) => mode, rawmode
154 (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
155 (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
156 (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
157 (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
158 (II, 1, (1,), 1, (1,), ()): ("1", "1"),
159 (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
160 (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
161 (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
162 (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
163 (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
164 (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
165 (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
166 (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
167 (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
168 (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
169 (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
170 (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
171 (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
172 (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
173 (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
174 (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
175 (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
176 (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
177 (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
178 (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
179 (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
180 (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
181 (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
182 (II, 1, (1,), 1, (8,), ()): ("L", "L"),
183 (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
184 (II, 1, (2,), 1, (8,), ()): ("L", "L"),
185 (MM, 1, (2,), 1, (8,), ()): ("L", "L"),
186 (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
187 (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
188 (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
189 (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
190 (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
191 (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
192 (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
193 (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
194 (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
195 (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
196 (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
197 (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
198 (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
199 (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
200 (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
201 (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
202 (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
203 (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
204 (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
205 (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
206 (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
207 (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
208 (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
209 (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
210 (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
211 (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
212 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
213 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
214 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
215 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
216 (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
217 (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
218 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
219 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
220 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
221 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
222 (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
223 (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
224 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
225 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
226 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
227 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
228 (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
229 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
230 (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
231 (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
232 (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
233 (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
234 (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
235 (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"),
236 (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
237 (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
238 (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
239 (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
240 (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
241 (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
242 (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
243 (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
244 (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
245 (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
246 (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
247 (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
248 (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
249 (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
250 (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
251 (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
252 (II, 3, (1,), 1, (8,), ()): ("P", "P"),
253 (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
254 (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
255 (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
256 (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
257 (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
258 (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
259 (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
260 (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
261 (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
262 (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
263 (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
264 (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
265 (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
266 (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
267 (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"),
268 (II, 6, (1,), 1, (8,), ()): ("L", "L"),
269 (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
270 # JPEG compressed images handled by LibTiff and auto-converted to RGBX
271 # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
272 (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
273 (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
274 (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
275 (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
276}
277
278MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
279
280PREFIXES = [
281 b"MM\x00\x2a", # Valid TIFF header with big-endian byte order
282 b"II\x2a\x00", # Valid TIFF header with little-endian byte order
283 b"MM\x2a\x00", # Invalid TIFF header, assume big-endian
284 b"II\x00\x2a", # Invalid TIFF header, assume little-endian
285 b"MM\x00\x2b", # BigTIFF with big-endian byte order
286 b"II\x2b\x00", # BigTIFF with little-endian byte order
287]
288
289
290def _accept(prefix: bytes) -> bool:
291 return prefix.startswith(tuple(PREFIXES))
292
293
294def _limit_rational(
295 val: float | Fraction | IFDRational, max_val: int
296) -> tuple[IntegralLike, IntegralLike]:
297 inv = abs(val) > 1
298 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
299 return n_d[::-1] if inv else n_d
300
301
302def _limit_signed_rational(
303 val: IFDRational, max_val: int, min_val: int
304) -> tuple[IntegralLike, IntegralLike]:
305 frac = Fraction(val)
306 n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator
307
308 if min(float(i) for i in n_d) < min_val:
309 n_d = _limit_rational(val, abs(min_val))
310
311 n_d_float = tuple(float(i) for i in n_d)
312 if max(n_d_float) > max_val:
313 n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val)
314
315 return n_d
316
317
318##
319# Wrapper for TIFF IFDs.
320
321_load_dispatch = {}
322_write_dispatch = {}
323
324
325def _delegate(op: str) -> Any:
326 def delegate(
327 self: IFDRational, *args: tuple[float, ...]
328 ) -> bool | float | Fraction:
329 return getattr(self._val, op)(*args)
330
331 return delegate
332
333
334class IFDRational(Rational):
335 """Implements a rational class where 0/0 is a legal value to match
336 the in the wild use of exif rationals.
337
338 e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
339 """
340
341 """ If the denominator is 0, store this as a float('nan'), otherwise store
342 as a fractions.Fraction(). Delegate as appropriate
343
344 """
345
346 __slots__ = ("_numerator", "_denominator", "_val")
347
348 def __init__(
349 self, value: float | Fraction | IFDRational, denominator: int = 1
350 ) -> None:
351 """
352 :param value: either an integer numerator, a
353 float/rational/other number, or an IFDRational
354 :param denominator: Optional integer denominator
355 """
356 self._val: Fraction | float
357 if isinstance(value, IFDRational):
358 self._numerator = value.numerator
359 self._denominator = value.denominator
360 self._val = value._val
361 return
362
363 if isinstance(value, Fraction):
364 self._numerator = value.numerator
365 self._denominator = value.denominator
366 else:
367 if TYPE_CHECKING:
368 self._numerator = cast(IntegralLike, value)
369 else:
370 self._numerator = value
371 self._denominator = denominator
372
373 if denominator == 0:
374 self._val = float("nan")
375 elif denominator == 1:
376 self._val = Fraction(value)
377 elif int(value) == value:
378 self._val = Fraction(int(value), denominator)
379 else:
380 self._val = Fraction(value / denominator)
381
382 @property
383 def numerator(self) -> IntegralLike:
384 return self._numerator
385
386 @property
387 def denominator(self) -> int:
388 return self._denominator
389
390 def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]:
391 """
392
393 :param max_denominator: Integer, the maximum denominator value
394 :returns: Tuple of (numerator, denominator)
395 """
396
397 if self.denominator == 0:
398 return self.numerator, self.denominator
399
400 assert isinstance(self._val, Fraction)
401 f = self._val.limit_denominator(max_denominator)
402 return f.numerator, f.denominator
403
404 def __repr__(self) -> str:
405 return str(float(self._val))
406
407 def __hash__(self) -> int: # type: ignore[override]
408 return self._val.__hash__()
409
410 def __eq__(self, other: object) -> bool:
411 val = self._val
412 if isinstance(other, IFDRational):
413 other = other._val
414 if isinstance(other, float):
415 val = float(val)
416 return val == other
417
418 def __getstate__(self) -> list[float | Fraction | IntegralLike]:
419 return [self._val, self._numerator, self._denominator]
420
421 def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None:
422 IFDRational.__init__(self, 0)
423 _val, _numerator, _denominator = state
424 assert isinstance(_val, (float, Fraction))
425 self._val = _val
426 if TYPE_CHECKING:
427 self._numerator = cast(IntegralLike, _numerator)
428 else:
429 self._numerator = _numerator
430 assert isinstance(_denominator, int)
431 self._denominator = _denominator
432
433 """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
434 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
435 'mod','rmod', 'pow','rpow', 'pos', 'neg',
436 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
437 'ceil', 'floor', 'round']
438 print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
439 """
440
441 __add__ = _delegate("__add__")
442 __radd__ = _delegate("__radd__")
443 __sub__ = _delegate("__sub__")
444 __rsub__ = _delegate("__rsub__")
445 __mul__ = _delegate("__mul__")
446 __rmul__ = _delegate("__rmul__")
447 __truediv__ = _delegate("__truediv__")
448 __rtruediv__ = _delegate("__rtruediv__")
449 __floordiv__ = _delegate("__floordiv__")
450 __rfloordiv__ = _delegate("__rfloordiv__")
451 __mod__ = _delegate("__mod__")
452 __rmod__ = _delegate("__rmod__")
453 __pow__ = _delegate("__pow__")
454 __rpow__ = _delegate("__rpow__")
455 __pos__ = _delegate("__pos__")
456 __neg__ = _delegate("__neg__")
457 __abs__ = _delegate("__abs__")
458 __trunc__ = _delegate("__trunc__")
459 __lt__ = _delegate("__lt__")
460 __gt__ = _delegate("__gt__")
461 __le__ = _delegate("__le__")
462 __ge__ = _delegate("__ge__")
463 __bool__ = _delegate("__bool__")
464 __ceil__ = _delegate("__ceil__")
465 __floor__ = _delegate("__floor__")
466 __round__ = _delegate("__round__")
467 # Python >= 3.11
468 if hasattr(Fraction, "__int__"):
469 __int__ = _delegate("__int__")
470
471
472_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
473
474
475def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
476 def decorator(func: _LoaderFunc) -> _LoaderFunc:
477 from .TiffTags import TYPES
478
479 if func.__name__.startswith("load_"):
480 TYPES[idx] = func.__name__[5:].replace("_", " ")
481 _load_dispatch[idx] = size, func # noqa: F821
482 return func
483
484 return decorator
485
486
487def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
488 def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
489 _write_dispatch[idx] = func # noqa: F821
490 return func
491
492 return decorator
493
494
495def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
496 from .TiffTags import TYPES
497
498 idx, fmt, name = idx_fmt_name
499 TYPES[idx] = name
500 size = struct.calcsize(f"={fmt}")
501
502 def basic_handler(
503 self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True
504 ) -> tuple[Any, ...]:
505 return self._unpack(f"{len(data) // size}{fmt}", data)
506
507 _load_dispatch[idx] = size, basic_handler # noqa: F821
508 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
509 b"".join(self._pack(fmt, value) for value in values)
510 )
511
512
513if TYPE_CHECKING:
514 _IFDv2Base = MutableMapping[int, Any]
515else:
516 _IFDv2Base = MutableMapping
517
518
519class ImageFileDirectory_v2(_IFDv2Base):
520 """This class represents a TIFF tag directory. To speed things up, we
521 don't decode tags unless they're asked for.
522
523 Exposes a dictionary interface of the tags in the directory::
524
525 ifd = ImageFileDirectory_v2()
526 ifd[key] = 'Some Data'
527 ifd.tagtype[key] = TiffTags.ASCII
528 print(ifd[key])
529 'Some Data'
530
531 Individual values are returned as the strings or numbers, sequences are
532 returned as tuples of the values.
533
534 The tiff metadata type of each item is stored in a dictionary of
535 tag types in
536 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
537 are read from a tiff file, guessed from the type added, or added
538 manually.
539
540 Data Structures:
541
542 * ``self.tagtype = {}``
543
544 * Key: numerical TIFF tag number
545 * Value: integer corresponding to the data type from
546 :py:data:`.TiffTags.TYPES`
547
548 .. versionadded:: 3.0.0
549
550 'Internal' data structures:
551
552 * ``self._tags_v2 = {}``
553
554 * Key: numerical TIFF tag number
555 * Value: decoded data, as tuple for multiple values
556
557 * ``self._tagdata = {}``
558
559 * Key: numerical TIFF tag number
560 * Value: undecoded byte string from file
561
562 * ``self._tags_v1 = {}``
563
564 * Key: numerical TIFF tag number
565 * Value: decoded data in the v1 format
566
567 Tags will be found in the private attributes ``self._tagdata``, and in
568 ``self._tags_v2`` once decoded.
569
570 ``self.legacy_api`` is a value for internal use, and shouldn't be changed
571 from outside code. In cooperation with
572 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
573 is true, then decoded tags will be populated into both ``_tags_v1`` and
574 ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
575 save routine. Tags should be read from ``_tags_v1`` if
576 ``legacy_api == true``.
577
578 """
579
580 _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
581 _write_dispatch: dict[int, Callable[..., Any]] = {}
582
583 def __init__(
584 self,
585 ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00",
586 prefix: bytes | None = None,
587 group: int | None = None,
588 ) -> None:
589 """Initialize an ImageFileDirectory.
590
591 To construct an ImageFileDirectory from a real file, pass the 8-byte
592 magic header to the constructor. To only set the endianness, pass it
593 as the 'prefix' keyword argument.
594
595 :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
596 endianness.
597 :param prefix: Override the endianness of the file.
598 """
599 if not _accept(ifh):
600 msg = f"not a TIFF file (header {repr(ifh)} not valid)"
601 raise SyntaxError(msg)
602 self._prefix = prefix if prefix is not None else ifh[:2]
603 if self._prefix == MM:
604 self._endian = ">"
605 elif self._prefix == II:
606 self._endian = "<"
607 else:
608 msg = "not a TIFF IFD"
609 raise SyntaxError(msg)
610 self._bigtiff = ifh[2] == 43
611 self.group = group
612 self.tagtype: dict[int, int] = {}
613 """ Dictionary of tag types """
614 self.reset()
615 self.next = (
616 self._unpack("Q", ifh[8:])[0]
617 if self._bigtiff
618 else self._unpack("L", ifh[4:])[0]
619 )
620 self._legacy_api = False
621
622 prefix = property(lambda self: self._prefix)
623 offset = property(lambda self: self._offset)
624
625 @property
626 def legacy_api(self) -> bool:
627 return self._legacy_api
628
629 @legacy_api.setter
630 def legacy_api(self, value: bool) -> NoReturn:
631 msg = "Not allowing setting of legacy api"
632 raise Exception(msg)
633
634 def reset(self) -> None:
635 self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
636 self._tags_v2: dict[int, Any] = {} # main tag storage
637 self._tagdata: dict[int, bytes] = {}
638 self.tagtype = {} # added 2008-06-05 by Florian Hoech
639 self._next = None
640 self._offset: int | None = None
641
642 def __str__(self) -> str:
643 return str(dict(self))
644
645 def named(self) -> dict[str, Any]:
646 """
647 :returns: dict of name|key: value
648
649 Returns the complete tag dictionary, with named tags where possible.
650 """
651 return {
652 TiffTags.lookup(code, self.group).name: value
653 for code, value in self.items()
654 }
655
656 def __len__(self) -> int:
657 return len(set(self._tagdata) | set(self._tags_v2))
658
659 def __getitem__(self, tag: int) -> Any:
660 if tag not in self._tags_v2: # unpack on the fly
661 data = self._tagdata[tag]
662 typ = self.tagtype[tag]
663 size, handler = self._load_dispatch[typ]
664 self[tag] = handler(self, data, self.legacy_api) # check type
665 val = self._tags_v2[tag]
666 if self.legacy_api and not isinstance(val, (tuple, bytes)):
667 val = (val,)
668 return val
669
670 def __contains__(self, tag: object) -> bool:
671 return tag in self._tags_v2 or tag in self._tagdata
672
673 def __setitem__(self, tag: int, value: Any) -> None:
674 self._setitem(tag, value, self.legacy_api)
675
676 def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None:
677 basetypes = (Number, bytes, str)
678
679 info = TiffTags.lookup(tag, self.group)
680 values = [value] if isinstance(value, basetypes) else value
681
682 if tag not in self.tagtype:
683 if info.type:
684 self.tagtype[tag] = info.type
685 else:
686 self.tagtype[tag] = TiffTags.UNDEFINED
687 if all(isinstance(v, IFDRational) for v in values):
688 for v in values:
689 assert isinstance(v, IFDRational)
690 if v < 0:
691 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL
692 break
693 else:
694 self.tagtype[tag] = TiffTags.RATIONAL
695 elif all(isinstance(v, int) for v in values):
696 short = True
697 signed_short = True
698 long = True
699 for v in values:
700 assert isinstance(v, int)
701 if short and not (0 <= v < 2**16):
702 short = False
703 if signed_short and not (-(2**15) < v < 2**15):
704 signed_short = False
705 if long and v < 0:
706 long = False
707 if short:
708 self.tagtype[tag] = TiffTags.SHORT
709 elif signed_short:
710 self.tagtype[tag] = TiffTags.SIGNED_SHORT
711 elif long:
712 self.tagtype[tag] = TiffTags.LONG
713 else:
714 self.tagtype[tag] = TiffTags.SIGNED_LONG
715 elif all(isinstance(v, float) for v in values):
716 self.tagtype[tag] = TiffTags.DOUBLE
717 elif all(isinstance(v, str) for v in values):
718 self.tagtype[tag] = TiffTags.ASCII
719 elif all(isinstance(v, bytes) for v in values):
720 self.tagtype[tag] = TiffTags.BYTE
721
722 if self.tagtype[tag] == TiffTags.UNDEFINED:
723 values = [
724 v.encode("ascii", "replace") if isinstance(v, str) else v
725 for v in values
726 ]
727 elif self.tagtype[tag] == TiffTags.RATIONAL:
728 values = [float(v) if isinstance(v, int) else v for v in values]
729
730 is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
731 if not is_ifd:
732 values = tuple(
733 info.cvt_enum(value) if isinstance(value, str) else value
734 for value in values
735 )
736
737 dest = self._tags_v1 if legacy_api else self._tags_v2
738
739 # Three branches:
740 # Spec'd length == 1, Actual length 1, store as element
741 # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
742 # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
743 # Don't mess with the legacy api, since it's frozen.
744 if not is_ifd and (
745 (info.length == 1)
746 or self.tagtype[tag] == TiffTags.BYTE
747 or (info.length is None and len(values) == 1 and not legacy_api)
748 ):
749 # Don't mess with the legacy api, since it's frozen.
750 if legacy_api and self.tagtype[tag] in [
751 TiffTags.RATIONAL,
752 TiffTags.SIGNED_RATIONAL,
753 ]: # rationals
754 values = (values,)
755 try:
756 (dest[tag],) = values
757 except ValueError:
758 # We've got a builtin tag with 1 expected entry
759 warnings.warn(
760 f"Metadata Warning, tag {tag} had too many entries: "
761 f"{len(values)}, expected 1"
762 )
763 dest[tag] = values[0]
764
765 else:
766 # Spec'd length > 1 or undefined
767 # Unspec'd, and length > 1
768 dest[tag] = values
769
770 def __delitem__(self, tag: int) -> None:
771 self._tags_v2.pop(tag, None)
772 self._tags_v1.pop(tag, None)
773 self._tagdata.pop(tag, None)
774
775 def __iter__(self) -> Iterator[int]:
776 return iter(set(self._tagdata) | set(self._tags_v2))
777
778 def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]:
779 return struct.unpack(self._endian + fmt, data)
780
781 def _pack(self, fmt: str, *values: Any) -> bytes:
782 return struct.pack(self._endian + fmt, *values)
783
784 list(
785 map(
786 _register_basic,
787 [
788 (TiffTags.SHORT, "H", "short"),
789 (TiffTags.LONG, "L", "long"),
790 (TiffTags.SIGNED_BYTE, "b", "signed byte"),
791 (TiffTags.SIGNED_SHORT, "h", "signed short"),
792 (TiffTags.SIGNED_LONG, "l", "signed long"),
793 (TiffTags.FLOAT, "f", "float"),
794 (TiffTags.DOUBLE, "d", "double"),
795 (TiffTags.IFD, "L", "long"),
796 (TiffTags.LONG8, "Q", "long8"),
797 ],
798 )
799 )
800
801 @_register_loader(1, 1) # Basic type, except for the legacy API.
802 def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes:
803 return data
804
805 @_register_writer(1) # Basic type, except for the legacy API.
806 def write_byte(self, data: bytes | int | IFDRational) -> bytes:
807 if isinstance(data, IFDRational):
808 data = int(data)
809 if isinstance(data, int):
810 data = bytes((data,))
811 return data
812
813 @_register_loader(2, 1)
814 def load_string(self, data: bytes, legacy_api: bool = True) -> str:
815 if data.endswith(b"\0"):
816 data = data[:-1]
817 return data.decode("latin-1", "replace")
818
819 @_register_writer(2)
820 def write_string(self, value: str | bytes | int) -> bytes:
821 # remerge of https://github.com/python-pillow/Pillow/pull/1416
822 if isinstance(value, int):
823 value = str(value)
824 if not isinstance(value, bytes):
825 value = value.encode("ascii", "replace")
826 return value + b"\0"
827
828 @_register_loader(5, 8)
829 def load_rational(
830 self, data: bytes, legacy_api: bool = True
831 ) -> tuple[tuple[int, int] | IFDRational, ...]:
832 vals = self._unpack(f"{len(data) // 4}L", data)
833
834 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
835 return (a, b) if legacy_api else IFDRational(a, b)
836
837 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
838
839 @_register_writer(5)
840 def write_rational(self, *values: IFDRational) -> bytes:
841 return b"".join(
842 self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
843 )
844
845 @_register_loader(7, 1)
846 def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes:
847 return data
848
849 @_register_writer(7)
850 def write_undefined(self, value: bytes | int | IFDRational) -> bytes:
851 if isinstance(value, IFDRational):
852 value = int(value)
853 if isinstance(value, int):
854 value = str(value).encode("ascii", "replace")
855 return value
856
857 @_register_loader(10, 8)
858 def load_signed_rational(
859 self, data: bytes, legacy_api: bool = True
860 ) -> tuple[tuple[int, int] | IFDRational, ...]:
861 vals = self._unpack(f"{len(data) // 4}l", data)
862
863 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
864 return (a, b) if legacy_api else IFDRational(a, b)
865
866 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
867
868 @_register_writer(10)
869 def write_signed_rational(self, *values: IFDRational) -> bytes:
870 return b"".join(
871 self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
872 for frac in values
873 )
874
875 def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
876 ret = fp.read(size)
877 if len(ret) != size:
878 msg = (
879 "Corrupt EXIF data. "
880 f"Expecting to read {size} bytes but only got {len(ret)}. "
881 )
882 raise OSError(msg)
883 return ret
884
885 def load(self, fp: IO[bytes]) -> None:
886 self.reset()
887 self._offset = fp.tell()
888
889 try:
890 tag_count = (
891 self._unpack("Q", self._ensure_read(fp, 8))
892 if self._bigtiff
893 else self._unpack("H", self._ensure_read(fp, 2))
894 )[0]
895 for i in range(tag_count):
896 tag, typ, count, data = (
897 self._unpack("HHQ8s", self._ensure_read(fp, 20))
898 if self._bigtiff
899 else self._unpack("HHL4s", self._ensure_read(fp, 12))
900 )
901
902 tagname = TiffTags.lookup(tag, self.group).name
903 typname = TYPES.get(typ, "unknown")
904 msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
905
906 try:
907 unit_size, handler = self._load_dispatch[typ]
908 except KeyError:
909 logger.debug("%s - unsupported type %s", msg, typ)
910 continue # ignore unsupported type
911 size = count * unit_size
912 if size > (8 if self._bigtiff else 4):
913 here = fp.tell()
914 (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
915 msg += f" Tag Location: {here} - Data Location: {offset}"
916 fp.seek(offset)
917 data = ImageFile._safe_read(fp, size)
918 fp.seek(here)
919 else:
920 data = data[:size]
921
922 if len(data) != size:
923 warnings.warn(
924 "Possibly corrupt EXIF data. "
925 f"Expecting to read {size} bytes but only got {len(data)}."
926 f" Skipping tag {tag}"
927 )
928 logger.debug(msg)
929 continue
930
931 if not data:
932 logger.debug(msg)
933 continue
934
935 self._tagdata[tag] = data
936 self.tagtype[tag] = typ
937
938 msg += " - value: "
939 msg += f"<table: {size} bytes>" if size > 32 else repr(data)
940
941 logger.debug(msg)
942
943 (self.next,) = (
944 self._unpack("Q", self._ensure_read(fp, 8))
945 if self._bigtiff
946 else self._unpack("L", self._ensure_read(fp, 4))
947 )
948 except OSError as msg:
949 warnings.warn(str(msg))
950 return
951
952 def _get_ifh(self) -> bytes:
953 ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
954 if self._bigtiff:
955 ifh += self._pack("HH", 8, 0)
956 ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
957
958 return ifh
959
960 def tobytes(self, offset: int = 0) -> bytes:
961 # FIXME What about tagdata?
962 result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
963
964 entries: list[tuple[int, int, int, bytes, bytes]] = []
965
966 fmt = "Q" if self._bigtiff else "L"
967 fmt_size = 8 if self._bigtiff else 4
968 offset += (
969 len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
970 )
971 stripoffsets = None
972
973 # pass 1: convert tags to binary format
974 # always write tags in ascending order
975 for tag, value in sorted(self._tags_v2.items()):
976 if tag == STRIPOFFSETS:
977 stripoffsets = len(entries)
978 typ = self.tagtype[tag]
979 logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
980 is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
981 if is_ifd:
982 ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
983 values = self._tags_v2[tag]
984 for ifd_tag, ifd_value in values.items():
985 ifd[ifd_tag] = ifd_value
986 data = ifd.tobytes(offset)
987 else:
988 values = value if isinstance(value, tuple) else (value,)
989 data = self._write_dispatch[typ](self, *values)
990
991 tagname = TiffTags.lookup(tag, self.group).name
992 typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
993 msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: "
994 msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values)
995 logger.debug(msg)
996
997 # count is sum of lengths for string and arbitrary data
998 if is_ifd:
999 count = 1
1000 elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
1001 count = len(data)
1002 else:
1003 count = len(values)
1004 # figure out if data fits into the entry
1005 if len(data) <= fmt_size:
1006 entries.append((tag, typ, count, data.ljust(fmt_size, b"\0"), b""))
1007 else:
1008 entries.append((tag, typ, count, self._pack(fmt, offset), data))
1009 offset += (len(data) + 1) // 2 * 2 # pad to word
1010
1011 # update strip offset data to point beyond auxiliary data
1012 if stripoffsets is not None:
1013 tag, typ, count, value, data = entries[stripoffsets]
1014 if data:
1015 size, handler = self._load_dispatch[typ]
1016 values = [val + offset for val in handler(self, data, self.legacy_api)]
1017 data = self._write_dispatch[typ](self, *values)
1018 else:
1019 value = self._pack(fmt, self._unpack(fmt, value)[0] + offset)
1020 entries[stripoffsets] = tag, typ, count, value, data
1021
1022 # pass 2: write entries to file
1023 for tag, typ, count, value, data in entries:
1024 logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
1025 result += self._pack(
1026 "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
1027 )
1028
1029 # -- overwrite here for multi-page --
1030 result += self._pack(fmt, 0) # end of entries
1031
1032 # pass 3: write auxiliary data to file
1033 for tag, typ, count, value, data in entries:
1034 result += data
1035 if len(data) & 1:
1036 result += b"\0"
1037
1038 return result
1039
1040 def save(self, fp: IO[bytes]) -> int:
1041 if fp.tell() == 0: # skip TIFF header on subsequent pages
1042 fp.write(self._get_ifh())
1043
1044 offset = fp.tell()
1045 result = self.tobytes(offset)
1046 fp.write(result)
1047 return offset + len(result)
1048
1049
1050ImageFileDirectory_v2._load_dispatch = _load_dispatch
1051ImageFileDirectory_v2._write_dispatch = _write_dispatch
1052for idx, name in TYPES.items():
1053 name = name.replace(" ", "_")
1054 setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
1055 setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
1056del _load_dispatch, _write_dispatch, idx, name
1057
1058
1059# Legacy ImageFileDirectory support.
1060class ImageFileDirectory_v1(ImageFileDirectory_v2):
1061 """This class represents the **legacy** interface to a TIFF tag directory.
1062
1063 Exposes a dictionary interface of the tags in the directory::
1064
1065 ifd = ImageFileDirectory_v1()
1066 ifd[key] = 'Some Data'
1067 ifd.tagtype[key] = TiffTags.ASCII
1068 print(ifd[key])
1069 ('Some Data',)
1070
1071 Also contains a dictionary of tag types as read from the tiff image file,
1072 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
1073
1074 Values are returned as a tuple.
1075
1076 .. deprecated:: 3.0.0
1077 """
1078
1079 def __init__(self, *args: Any, **kwargs: Any) -> None:
1080 super().__init__(*args, **kwargs)
1081 self._legacy_api = True
1082
1083 tags = property(lambda self: self._tags_v1)
1084 tagdata = property(lambda self: self._tagdata)
1085
1086 # defined in ImageFileDirectory_v2
1087 tagtype: dict[int, int]
1088 """Dictionary of tag types"""
1089
1090 @classmethod
1091 def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1:
1092 """Returns an
1093 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1094 instance with the same data as is contained in the original
1095 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1096 instance.
1097
1098 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1099
1100 """
1101
1102 ifd = cls(prefix=original.prefix)
1103 ifd._tagdata = original._tagdata
1104 ifd.tagtype = original.tagtype
1105 ifd.next = original.next # an indicator for multipage tiffs
1106 return ifd
1107
1108 def to_v2(self) -> ImageFileDirectory_v2:
1109 """Returns an
1110 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1111 instance with the same data as is contained in the original
1112 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1113 instance.
1114
1115 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1116
1117 """
1118
1119 ifd = ImageFileDirectory_v2(prefix=self.prefix)
1120 ifd._tagdata = dict(self._tagdata)
1121 ifd.tagtype = dict(self.tagtype)
1122 ifd._tags_v2 = dict(self._tags_v2)
1123 return ifd
1124
1125 def __contains__(self, tag: object) -> bool:
1126 return tag in self._tags_v1 or tag in self._tagdata
1127
1128 def __len__(self) -> int:
1129 return len(set(self._tagdata) | set(self._tags_v1))
1130
1131 def __iter__(self) -> Iterator[int]:
1132 return iter(set(self._tagdata) | set(self._tags_v1))
1133
1134 def __setitem__(self, tag: int, value: Any) -> None:
1135 for legacy_api in (False, True):
1136 self._setitem(tag, value, legacy_api)
1137
1138 def __getitem__(self, tag: int) -> Any:
1139 if tag not in self._tags_v1: # unpack on the fly
1140 data = self._tagdata[tag]
1141 typ = self.tagtype[tag]
1142 size, handler = self._load_dispatch[typ]
1143 for legacy in (False, True):
1144 self._setitem(tag, handler(self, data, legacy), legacy)
1145 val = self._tags_v1[tag]
1146 if not isinstance(val, (tuple, bytes)):
1147 val = (val,)
1148 return val
1149
1150
1151# undone -- switch this pointer
1152ImageFileDirectory = ImageFileDirectory_v1
1153
1154
1155##
1156# Image plugin for TIFF files.
1157
1158
1159class TiffImageFile(ImageFile.ImageFile):
1160 format = "TIFF"
1161 format_description = "Adobe TIFF"
1162 _close_exclusive_fp_after_loading = False
1163
1164 def __init__(
1165 self,
1166 fp: StrOrBytesPath | IO[bytes],
1167 filename: str | bytes | None = None,
1168 ) -> None:
1169 self.tag_v2: ImageFileDirectory_v2
1170 """ Image file directory (tag dictionary) """
1171
1172 self.tag: ImageFileDirectory_v1
1173 """ Legacy tag entries """
1174
1175 super().__init__(fp, filename)
1176
1177 def _open(self) -> None:
1178 """Open the first image in a TIFF file"""
1179
1180 # Header
1181 assert self.fp is not None
1182 ifh = self.fp.read(8)
1183 if ifh[2] == 43:
1184 ifh += self.fp.read(8)
1185
1186 self.tag_v2 = ImageFileDirectory_v2(ifh)
1187
1188 # setup frame pointers
1189 self.__first = self.__next = self.tag_v2.next
1190 self.__frame = -1
1191 self._fp = self.fp
1192 self._frame_pos: list[int] = []
1193 self._n_frames: int | None = None
1194
1195 logger.debug("*** TiffImageFile._open ***")
1196 logger.debug("- __first: %s", self.__first)
1197 logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
1198
1199 # and load the first frame
1200 self._seek(0)
