Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
TiffImagePlugin.py2354 linesDownload Raw Back to PIL
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)

Showing the first 1,200 of 2354 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai