Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/PIL/TiffImagePlugin.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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
43__lazy_modules__ = {
44 "PIL._binary",
45 "PIL._util",
46 "fractions",
47 "itertools",
48 "math",
49 "struct",
50 "warnings",
51}
53import io
54import itertools
55import logging
56import math
57import os
58import struct
59import warnings
60from collections.abc import Callable, MutableMapping
61from fractions import Fraction
62from numbers import Number, Rational
63from typing import IO, Any, cast
65from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
66from ._binary import i16be as i16
67from ._binary import i32be as i32
68from ._util import DeferredError, is_path
69from .TiffTags import TYPES
71TYPE_CHECKING = False
72if TYPE_CHECKING:
73 from collections.abc import Iterator
74 from typing import NoReturn, Self
76 from ._typing import Buffer, IntegralLike, StrOrBytesPath
78logger = logging.getLogger(__name__)
80# Set these to true to force use of libtiff for reading or writing.
81READ_LIBTIFF = False
82WRITE_LIBTIFF = False
83STRIP_SIZE = 65536
85II = b"II" # little-endian (Intel style)
86MM = b"MM" # big-endian (Motorola style)
88#
89# --------------------------------------------------------------------
90# Read TIFF files
92# a few tag names, just to make the code below a bit more readable
93OSUBFILETYPE = 255
94IMAGEWIDTH = 256
95IMAGELENGTH = 257
96BITSPERSAMPLE = 258
97COMPRESSION = 259
98PHOTOMETRIC_INTERPRETATION = 262
99FILLORDER = 266
100IMAGEDESCRIPTION = 270
101STRIPOFFSETS = 273
102SAMPLESPERPIXEL = 277
103ROWSPERSTRIP = 278
104STRIPBYTECOUNTS = 279
105X_RESOLUTION = 282
106Y_RESOLUTION = 283
107PLANAR_CONFIGURATION = 284
108RESOLUTION_UNIT = 296
109TRANSFERFUNCTION = 301
110SOFTWARE = 305
111DATE_TIME = 306
112ARTIST = 315
113PREDICTOR = 317
114COLORMAP = 320
115TILEWIDTH = 322
116TILELENGTH = 323
117TILEOFFSETS = 324
118TILEBYTECOUNTS = 325
119SUBIFD = 330
120EXTRASAMPLES = 338
121SAMPLEFORMAT = 339
122JPEGTABLES = 347
123YCBCRSUBSAMPLING = 530
124REFERENCEBLACKWHITE = 532
125COPYRIGHT = 33432
126IPTC_NAA_CHUNK = 33723 # newsphoto properties
127PHOTOSHOP_CHUNK = 34377 # photoshop properties
128ICCPROFILE = 34675
129EXIFIFD = 34665
130XMP = 700
131JPEGQUALITY = 65537 # pseudo-tag by libtiff
133# https://github.com/imagej/ImageJ/blob/master/ij/io/TiffDecoder.java
134IMAGEJ_META_DATA_BYTE_COUNTS = 50838
135IMAGEJ_META_DATA = 50839
137COMPRESSION_INFO = {
138 # Compression => pil compression name
139 1: "raw",
140 2: "tiff_ccitt",
141 3: "group3",
142 4: "group4",
143 5: "tiff_lzw",
144 6: "tiff_jpeg", # obsolete
145 7: "jpeg",
146 8: "tiff_adobe_deflate",
147 32771: "tiff_raw_16", # 16-bit padding
148 32773: "packbits",
149 32809: "tiff_thunderscan",
150 32946: "tiff_deflate",
151 34676: "tiff_sgilog",
152 34677: "tiff_sgilog24",
153 34925: "lzma",
154 50000: "zstd",
155 50001: "webp",
156}
158COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
160OPEN_INFO = {
161 # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
162 # ExtraSamples) => mode, rawmode
163 (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
164 (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
165 (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
166 (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
167 (II, 1, (1,), 1, (1,), ()): ("1", "1"),
168 (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
169 (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
170 (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
171 (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
172 (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
173 (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
174 (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
175 (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
176 (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
177 (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
178 (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
179 (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
180 (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
181 (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
182 (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
183 (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
184 (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
185 (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
186 (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
187 (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
188 (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
189 (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
190 (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
191 (II, 1, (1,), 1, (8,), ()): ("L", "L"),
192 (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
193 (II, 1, (2,), 1, (8,), ()): ("L", "L"),
194 (MM, 1, (2,), 1, (8,), ()): ("L", "L"),
195 (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
196 (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
197 (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
198 (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
199 (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
200 (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
201 (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
202 (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
203 (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
204 (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
205 (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
206 (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
207 (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
208 (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
209 (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
210 (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
211 (II, 1, (1,), 1, (8, 8), (0,)): ("L", "LX"),
212 (MM, 1, (1,), 1, (8, 8), (0,)): ("L", "LX"),
213 (II, 1, (1,), 1, (8, 8), (1,)): ("La", "La"),
214 (MM, 1, (1,), 1, (8, 8), (1,)): ("La", "La"),
215 (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
216 (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
217 (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
218 (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
219 (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
220 (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
221 (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
222 (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples
223 (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
224 (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGB", "RGBX"),
225 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
226 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGB", "RGBXX"),
227 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
228 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGB", "RGBXXX"),
229 (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
230 (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
231 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
232 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
233 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
234 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
235 (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
236 (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
237 (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
238 (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
239 (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
240 (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
241 (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
242 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
243 (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
244 (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
245 (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
246 (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
247 (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16L"),
248 (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGB", "RGBX;16B"),
249 (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
250 (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
251 (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
252 (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
253 (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
254 (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
255 (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
256 (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
257 (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
258 (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
259 (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
260 (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
261 (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
262 (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
263 (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
264 (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
265 (II, 3, (1,), 1, (8,), ()): ("P", "P"),
266 (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
267 (II, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
268 (MM, 3, (1,), 1, (8, 8), (0,)): ("P", "PX"),
269 (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
270 (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
271 (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
272 (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
273 (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
274 (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
275 (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
276 (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
277 (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
278 (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
279 (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
280 (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"),
281 (II, 6, (1,), 1, (8,), ()): ("L", "L"),
282 (MM, 6, (1,), 1, (8,), ()): ("L", "L"),
283 (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
284 (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
285 (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
286 (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
287}
289MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
291PREFIXES = [
292 b"MM\x00\x2a", # Valid TIFF header with big-endian byte order
293 b"II\x2a\x00", # Valid TIFF header with little-endian byte order
294 b"MM\x2a\x00", # Invalid TIFF header, assume big-endian
295 b"II\x00\x2a", # Invalid TIFF header, assume little-endian
296 b"MM\x00\x2b", # BigTIFF with big-endian byte order
297 b"II\x2b\x00", # BigTIFF with little-endian byte order
298]
301def _accept(prefix: bytes) -> bool:
302 return prefix.startswith(tuple(PREFIXES))
305def _limit_rational(
306 val: float | Fraction | IFDRational, max_val: int
307) -> tuple[IntegralLike, IntegralLike]:
308 inv = abs(val) > 1
309 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
310 return n_d[::-1] if inv else n_d
313def _limit_signed_rational(
314 val: IFDRational, max_val: int, min_val: int
315) -> tuple[IntegralLike, IntegralLike]:
316 frac = Fraction(val)
317 n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator
319 if min(float(i) for i in n_d) < min_val:
320 n_d = _limit_rational(val, abs(min_val))
322 n_d_float = tuple(float(i) for i in n_d)
323 if max(n_d_float) > max_val:
324 n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val)
326 return n_d
329##
330# Wrapper for TIFF IFDs.
332_load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
333_write_dispatch: dict[int, Callable[..., Any]] = {}
336def _delegate(op: str) -> Any:
337 def delegate(
338 self: IFDRational, *args: tuple[float, ...]
339 ) -> bool | float | Fraction:
340 func = getattr(self._val, op)
341 if len(args) == 1 and isinstance(args[0], IFDRational):
342 return func(args[0]._val)
343 return func(*args)
345 return delegate
348class IFDRational(Rational):
349 """Implements a rational class where 0/0 is a legal value to match
350 the in the wild use of exif rationals.
352 e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
353 """
355 """ If the denominator is 0, store this as a float('nan'), otherwise store
356 as a fractions.Fraction(). Delegate as appropriate
358 """
360 __slots__ = ("_numerator", "_denominator", "_val")
362 def __init__(
363 self, value: float | Fraction | IFDRational, denominator: int = 1
364 ) -> None:
365 """
366 :param value: either an integer numerator, a
367 float/rational/other number, or an IFDRational
368 :param denominator: Optional integer denominator
369 """
370 self._val: Fraction | float
371 if isinstance(value, IFDRational):
372 self._numerator = value.numerator
373 self._denominator = value.denominator
374 self._val = value._val
375 return
377 if isinstance(value, Fraction):
378 self._numerator = value.numerator
379 self._denominator = value.denominator
380 else:
381 if TYPE_CHECKING:
382 self._numerator = cast("IntegralLike", value)
383 else:
384 self._numerator = value
385 self._denominator = denominator
387 if denominator == 0:
388 self._val = float("nan")
389 elif denominator == 1:
390 self._val = Fraction(value)
391 elif int(value) == value:
392 self._val = Fraction(int(value), denominator)
393 else:
394 self._val = Fraction(value / denominator)
396 @property
397 def numerator(self) -> IntegralLike:
398 return self._numerator
400 @property
401 def denominator(self) -> int:
402 return self._denominator
404 def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]:
405 """
407 :param max_denominator: Integer, the maximum denominator value
408 :returns: Tuple of (numerator, denominator)
409 """
411 if self.denominator == 0:
412 return self.numerator, self.denominator
414 assert isinstance(self._val, Fraction)
415 f = self._val.limit_denominator(max_denominator)
416 return f.numerator, f.denominator
418 def __repr__(self) -> str:
419 return str(float(self._val))
421 def __hash__(self) -> int: # type: ignore[override]
422 return self._val.__hash__()
424 def __eq__(self, other: object) -> bool:
425 val = self._val
426 if isinstance(other, IFDRational):
427 other = other._val
428 if isinstance(other, float):
429 val = float(val)
430 return val == other
432 def __getstate__(self) -> list[float | Fraction | IntegralLike]:
433 return [self._val, self._numerator, self._denominator]
435 def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None:
436 IFDRational.__init__(self, 0)
437 _val, _numerator, _denominator = state
438 assert isinstance(_val, (float, Fraction))
439 self._val = _val
440 if TYPE_CHECKING:
441 self._numerator = cast("IntegralLike", _numerator)
442 else:
443 self._numerator = _numerator
444 assert isinstance(_denominator, int)
445 self._denominator = _denominator
447 """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
448 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
449 'mod','rmod', 'pow','rpow', 'pos', 'neg',
450 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
451 'ceil', 'floor', 'round']
452 print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
453 """
455 __add__ = _delegate("__add__")
456 __radd__ = _delegate("__radd__")
457 __sub__ = _delegate("__sub__")
458 __rsub__ = _delegate("__rsub__")
459 __mul__ = _delegate("__mul__")
460 __rmul__ = _delegate("__rmul__")
461 __truediv__ = _delegate("__truediv__")
462 __rtruediv__ = _delegate("__rtruediv__")
463 __floordiv__ = _delegate("__floordiv__")
464 __rfloordiv__ = _delegate("__rfloordiv__")
465 __mod__ = _delegate("__mod__")
466 __rmod__ = _delegate("__rmod__")
467 __pow__ = _delegate("__pow__")
468 __rpow__ = _delegate("__rpow__")
469 __pos__ = _delegate("__pos__")
470 __neg__ = _delegate("__neg__")
471 __abs__ = _delegate("__abs__")
472 __trunc__ = _delegate("__trunc__")
473 __lt__ = _delegate("__lt__")
474 __gt__ = _delegate("__gt__")
475 __le__ = _delegate("__le__")
476 __ge__ = _delegate("__ge__")
477 __bool__ = _delegate("__bool__")
478 __ceil__ = _delegate("__ceil__")
479 __floor__ = _delegate("__floor__")
480 __round__ = _delegate("__round__")
481 __float__ = _delegate("__float__")
482 __int__ = _delegate("__int__")
485_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
488def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
489 def decorator(func: _LoaderFunc) -> _LoaderFunc:
490 from .TiffTags import TYPES
492 if func.__name__.startswith("load_"):
493 TYPES[idx] = func.__name__[5:].replace("_", " ")
494 _load_dispatch[idx] = size, func # noqa: F821
495 return func
497 return decorator
500def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
501 def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
502 _write_dispatch[idx] = func # noqa: F821
503 return func
505 return decorator
508def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
509 from .TiffTags import TYPES
511 idx, fmt, name = idx_fmt_name
512 TYPES[idx] = name
513 size = struct.calcsize(f"={fmt}")
515 def basic_handler(
516 self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True
517 ) -> tuple[Any, ...]:
518 return self._unpack(f"{len(data) // size}{fmt}", data)
520 _load_dispatch[idx] = size, basic_handler # noqa: F821
521 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
522 b"".join(self._pack(fmt, value) for value in values)
523 )
526if TYPE_CHECKING:
527 _IFDv2Base = MutableMapping[int, Any]
528else:
529 _IFDv2Base = MutableMapping
532class ImageFileDirectory_v2(_IFDv2Base):
533 """This class represents a TIFF tag directory. To speed things up, we
534 don't decode tags unless they're asked for.
536 Exposes a dictionary interface of the tags in the directory::
538 ifd = ImageFileDirectory_v2()
539 ifd[key] = 'Some Data'
540 ifd.tagtype[key] = TiffTags.ASCII
541 print(ifd[key])
542 'Some Data'
544 Individual values are returned as the strings or numbers, sequences are
545 returned as tuples of the values.
547 The tiff metadata type of each item is stored in a dictionary of
548 tag types in
549 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
550 are read from a tiff file, guessed from the type added, or added
551 manually.
553 Data Structures:
555 * ``self.tagtype = {}``
557 * Key: numerical TIFF tag number
558 * Value: integer corresponding to the data type from
559 :py:data:`.TiffTags.TYPES`
561 .. versionadded:: 3.0.0
563 'Internal' data structures:
565 * ``self._tags_v2 = {}``
567 * Key: numerical TIFF tag number
568 * Value: decoded data, as tuple for multiple values
570 * ``self._tagdata = {}``
572 * Key: numerical TIFF tag number
573 * Value: undecoded byte string from file
575 * ``self._tags_v1 = {}``
577 * Key: numerical TIFF tag number
578 * Value: decoded data in the v1 format
580 Tags will be found in the private attributes ``self._tagdata``, and in
581 ``self._tags_v2`` once decoded.
583 ``self.legacy_api`` is a value for internal use, and shouldn't be changed
584 from outside code. In cooperation with
585 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
586 is true, then decoded tags will be populated into both ``_tags_v1`` and
587 ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
588 save routine. Tags should be read from ``_tags_v1`` if
589 ``legacy_api == true``.
591 """
593 _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
594 _write_dispatch: dict[int, Callable[..., Any]] = {}
596 def __init__(
597 self,
598 ifh: bytes = b"II\x2a\x00\x00\x00\x00\x00",
599 prefix: bytes | None = None,
600 group: int | None = None,
601 ) -> None:
602 """Initialize an ImageFileDirectory.
604 To construct an ImageFileDirectory from a real file, pass the 8-byte
605 magic header to the constructor. To only set the endianness, pass it
606 as the 'prefix' keyword argument.
608 :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
609 endianness.
610 :param prefix: Override the endianness of the file.
611 """
612 if not _accept(ifh):
613 msg = f"not a TIFF file (header {repr(ifh)} not valid)"
614 raise SyntaxError(msg)
615 self._prefix = prefix if prefix is not None else ifh[:2]
616 if self._prefix == MM:
617 self._endian = ">"
618 elif self._prefix == II:
619 self._endian = "<"
620 else:
621 msg = "not a TIFF IFD"
622 raise SyntaxError(msg)
623 self._bigtiff = 43 in ifh[2:4]
624 self.group = group
625 self.tagtype: dict[int, int] = {}
626 """ Dictionary of tag types """
627 self.reset()
628 # Maximum total out-of-line tag data that load() may read, as well
629 # as the size of the stream. Set by TiffImageFile so that
630 # all frames of a file share one budget.
631 self._tag_data_limit: int | None = None
632 self.next = (
633 self._unpack("Q", ifh[8:])[0]
634 if self._bigtiff
635 else self._unpack("L", ifh[4:])[0]
636 )
637 self._legacy_api = False
639 prefix = property(lambda self: self._prefix)
640 offset = property(lambda self: self._offset)
642 @property
643 def legacy_api(self) -> bool:
644 return self._legacy_api
646 @legacy_api.setter
647 def legacy_api(self, value: bool) -> NoReturn:
648 msg = "Not allowing setting of legacy api"
649 raise Exception(msg)
651 def reset(self) -> None:
652 self._tags_v1: dict[int, Any] = {} # will remain empty if legacy_api is false
653 self._tags_v2: dict[int, Any] = {} # main tag storage
654 self._tagdata: dict[int, bytes] = {}
655 self.tagtype = {} # added 2008-06-05 by Florian Hoech
656 self._next = None
657 self._offset: int | None = None
659 def __str__(self) -> str:
660 return str(dict(self))
662 def named(self) -> dict[str, Any]:
663 """
664 :returns: dict of name|key: value
666 Returns the complete tag dictionary, with named tags where possible.
667 """
668 return {
669 TiffTags.lookup(code, self.group).name: value
670 for code, value in self.items()
671 }
673 def __len__(self) -> int:
674 return len(set(self._tagdata) | set(self._tags_v2))
676 def __getitem__(self, tag: int) -> Any:
677 if tag not in self._tags_v2: # unpack on the fly
678 data = self._tagdata[tag]
679 typ = self.tagtype[tag]
680 size, handler = self._load_dispatch[typ]
681 self[tag] = handler(self, data, self.legacy_api) # check type
682 val = self._tags_v2[tag]
683 if self.legacy_api and not isinstance(val, (tuple, bytes)):
684 val = (val,)
685 return val
687 def __contains__(self, tag: object) -> bool:
688 return tag in self._tags_v2 or tag in self._tagdata
690 def __setitem__(self, tag: int, value: Any) -> None:
691 self._setitem(tag, value, self.legacy_api)
693 def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None:
694 basetypes = (Number, bytes, str)
696 info = TiffTags.lookup(tag, self.group)
697 values = [value] if isinstance(value, basetypes) else value
699 if tag not in self.tagtype:
700 if info.type:
701 self.tagtype[tag] = info.type
702 else:
703 self.tagtype[tag] = TiffTags.UNDEFINED
704 if all(isinstance(v, IFDRational) for v in values):
705 for v in values:
706 value = (
707 v.numerator
708 if isinstance(v, IFDRational) and v.denominator == 0
709 else v
710 )
711 if value < IFDRational(0):
712 self.tagtype[tag] = TiffTags.SIGNED_RATIONAL
713 break
714 else:
715 self.tagtype[tag] = TiffTags.RATIONAL
716 elif all(isinstance(v, int) for v in values):
717 short = True
718 signed_short = True
719 long = True
720 for v in values:
721 assert isinstance(v, int)
722 if short and not (0 <= v < 2**16):
723 short = False
724 if signed_short and not (-(2**15) < v < 2**15):
725 signed_short = False
726 if long and v < 0:
727 long = False
728 if short:
729 self.tagtype[tag] = TiffTags.SHORT
730 elif signed_short:
731 self.tagtype[tag] = TiffTags.SIGNED_SHORT
732 elif long:
733 self.tagtype[tag] = TiffTags.LONG
734 else:
735 self.tagtype[tag] = TiffTags.SIGNED_LONG
736 elif all(isinstance(v, float) for v in values):
737 self.tagtype[tag] = TiffTags.DOUBLE
738 elif all(isinstance(v, str) for v in values):
739 self.tagtype[tag] = TiffTags.ASCII
740 elif all(isinstance(v, bytes) for v in values):
741 self.tagtype[tag] = TiffTags.BYTE
743 if self.tagtype[tag] == TiffTags.UNDEFINED:
744 values = [
745 v.encode("ascii", "replace") if isinstance(v, str) else v
746 for v in values
747 ]
748 elif self.tagtype[tag] == TiffTags.RATIONAL:
749 values = [float(v) if isinstance(v, int) else v for v in values]
751 is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
752 if not is_ifd:
753 values = tuple(
754 info.cvt_enum(value) if isinstance(value, str) else value
755 for value in values
756 )
758 dest = self._tags_v1 if legacy_api else self._tags_v2
760 # Three branches:
761 # Spec'd length == 1, Actual length 1, store as element
762 # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
763 # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
764 # Don't mess with the legacy api, since it's frozen.
765 if not is_ifd and (
766 (info.length == 1)
767 or self.tagtype[tag] == TiffTags.BYTE
768 or (info.length is None and len(values) == 1 and not legacy_api)
769 ):
770 # Don't mess with the legacy api, since it's frozen.
771 if legacy_api and self.tagtype[tag] in [
772 TiffTags.RATIONAL,
773 TiffTags.SIGNED_RATIONAL,
774 ]: # rationals
775 values = (values,)
776 try:
777 (dest[tag],) = values
778 except ValueError:
779 # We've got a builtin tag with 1 expected entry
780 if tag != PHOTOMETRIC_INTERPRETATION and self.group is None:
781 warnings.warn(
782 f"Metadata Warning, tag {tag} had too many entries: "
783 f"{len(values)}, expected 1"
784 )
785 dest[tag] = values[0]
787 else:
788 # Spec'd length > 1 or undefined
789 # Unspec'd, and length > 1
790 dest[tag] = values
792 def __delitem__(self, tag: int) -> None:
793 self._tags_v2.pop(tag, None)
794 self._tags_v1.pop(tag, None)
795 self._tagdata.pop(tag, None)
797 def __iter__(self) -> Iterator[int]:
798 return iter(set(self._tagdata) | set(self._tags_v2))
800 def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]:
801 return struct.unpack(self._endian + fmt, data)
803 def _pack(self, fmt: str, *values: Any) -> bytes:
804 return struct.pack(self._endian + fmt, *values)
806 list(
807 map(
808 _register_basic,
809 [
810 (TiffTags.SHORT, "H", "short"),
811 (TiffTags.LONG, "L", "long"),
812 (TiffTags.SIGNED_BYTE, "b", "signed byte"),
813 (TiffTags.SIGNED_SHORT, "h", "signed short"),
814 (TiffTags.SIGNED_LONG, "l", "signed long"),
815 (TiffTags.FLOAT, "f", "float"),
816 (TiffTags.DOUBLE, "d", "double"),
817 (TiffTags.IFD, "L", "long"),
818 (TiffTags.LONG8, "Q", "long8"),
819 ],
820 )
821 )
823 @_register_loader(1, 1) # Basic type, except for the legacy API.
824 def load_byte(self, data: bytes, legacy_api: bool = True) -> bytes:
825 return data
827 @_register_writer(1) # Basic type, except for the legacy API.
828 def write_byte(self, data: bytes | int | IFDRational) -> bytes:
829 if isinstance(data, IFDRational):
830 data = int(data)
831 if isinstance(data, int):
832 data = bytes((data,))
833 return data
835 @_register_loader(2, 1)
836 def load_string(self, data: bytes, legacy_api: bool = True) -> str:
837 if data.endswith(b"\0"):
838 data = data[:-1]
839 return data.decode("latin-1", "replace")
841 @_register_writer(2)
842 def write_string(self, value: str | bytes | int) -> bytes:
843 # remerge of https://github.com/python-pillow/Pillow/pull/1416
844 if isinstance(value, int):
845 value = str(value)
846 if not isinstance(value, bytes):
847 value = value.encode("ascii", "replace")
848 return value + b"\0"
850 @_register_loader(5, 8)
851 def load_rational(
852 self, data: bytes, legacy_api: bool = True
853 ) -> tuple[tuple[int, int] | IFDRational, ...]:
854 vals = self._unpack(f"{len(data) // 4}L", data)
856 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
857 return (a, b) if legacy_api else IFDRational(a, b)
859 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
861 @_register_writer(5)
862 def write_rational(self, *values: IFDRational) -> bytes:
863 return b"".join(
864 self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
865 )
867 @_register_loader(7, 1)
868 def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes:
869 return data
871 @_register_writer(7)
872 def write_undefined(self, value: bytes | int | IFDRational) -> bytes:
873 if isinstance(value, IFDRational):
874 value = int(value)
875 if isinstance(value, int):
876 value = str(value).encode("ascii", "replace")
877 return value
879 @_register_loader(10, 8)
880 def load_signed_rational(
881 self, data: bytes, legacy_api: bool = True
882 ) -> tuple[tuple[int, int] | IFDRational, ...]:
883 vals = self._unpack(f"{len(data) // 4}l", data)
885 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
886 return (a, b) if legacy_api else IFDRational(a, b)
888 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
890 @_register_writer(10)
891 def write_signed_rational(self, *values: IFDRational) -> bytes:
892 return b"".join(
893 self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
894 for frac in values
895 )
897 def _ensure_read(self, fp: IO[bytes], size: int) -> bytes:
898 ret = fp.read(size)
899 if len(ret) != size:
900 msg = (
901 "Corrupt EXIF data. "
902 f"Expecting to read {size} bytes but only got {len(ret)}. "
903 )
904 raise OSError(msg)
905 return ret
907 def load(self, fp: IO[bytes]) -> None:
908 self.reset()
909 self._offset = fp.tell()
910 limit = ImageFile._get_fp_length(fp)
911 if self._tag_data_limit is not None:
912 limit = min(limit, self._tag_data_limit)
913 self._tag_data_size = 0
915 try:
916 tag_count = (
917 self._unpack("Q", self._ensure_read(fp, 8))
918 if self._bigtiff
919 else self._unpack("H", self._ensure_read(fp, 2))
920 )[0]
921 total_size = 0
922 seen_tags = set()
923 for i in range(tag_count):
924 tag, typ, count, data = (
925 self._unpack("HHQ8s", self._ensure_read(fp, 20))
926 if self._bigtiff
927 else self._unpack("HHL4s", self._ensure_read(fp, 12))
928 )
929 if tag in seen_tags:
930 # Ignore duplicate tags, like libtiff
931 # even if the first one was skipped
932 continue
933 seen_tags.add(tag)
934 if (
935 tag == PHOTOMETRIC_INTERPRETATION
936 and count != 1
937 and self.group is None
938 ):
939 # Ignore a malformed PhotometricInterpretation tag, like libtiff
940 continue
942 # tagname = TiffTags.lookup(tag, self.group).name
943 # typname = TYPES.get(typ, "unknown")
944 # msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
946 try:
947 unit_size, handler = self._load_dispatch[typ]
948 except KeyError:
949 # logger.debug("%s - unsupported type %s", msg, typ)
950 continue # ignore unsupported type
951 size = count * unit_size
952 if size > (8 if self._bigtiff else 4):
953 here = fp.tell()
954 (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
955 # msg += f" Tag Location: {here} - Data Location: {offset}"
956 if offset >= 2**63:
957 warnings.warn("Tag offset too large")
958 continue
959 # Well-formed IFD values should fit within the stream they
960 # are read from, even before accounting for overlaps.
961 total_size += size
962 if total_size > limit:
963 warnings.warn(f"Tag data larger than file. Skipping tag {tag}")
964 continue
965 self._tag_data_size += size
966 fp.seek(offset)
967 data = ImageFile._safe_read(fp, size)
968 fp.seek(here)
969 else:
970 data = data[:size]
972 if len(data) != size:
973 warnings.warn(
974 "Possibly corrupt EXIF data. "
975 f"Expecting to read {size} bytes but only got {len(data)}."
976 f" Skipping tag {tag}"
977 )
978 # logger.debug(msg)
979 continue
981 if not data:
982 # logger.debug(msg)
983 continue
985 self._tagdata[tag] = data
986 self.tagtype[tag] = typ
988 # msg += " - value: "
989 # msg += f"<table: {size} bytes>" if size > 32 else repr(data)
991 # logger.debug(msg)
993 (self.next,) = (
994 self._unpack("Q", self._ensure_read(fp, 8))
995 if self._bigtiff
996 else self._unpack("L", self._ensure_read(fp, 4))
997 )
998 except OSError as msg:
999 warnings.warn(str(msg))
1000 return
1002 def _get_ifh(self) -> bytes:
1003 ifh = self._prefix + self._pack("H", 43 if self._bigtiff else 42)
1004 if self._bigtiff:
1005 ifh += self._pack("HH", 8, 0)
1006 ifh += self._pack("Q", 16) if self._bigtiff else self._pack("L", 8)
1008 return ifh
1010 def tobytes(self, offset: int = 0) -> bytes:
1011 # FIXME What about tagdata?
1012 result = self._pack("Q" if self._bigtiff else "H", len(self._tags_v2))
1014 entries: list[tuple[int, int, int, bytes, bytes]] = []
1016 fmt = "Q" if self._bigtiff else "L"
1017 fmt_size = 8 if self._bigtiff else 4
1018 offset += (
1019 len(result) + len(self._tags_v2) * (20 if self._bigtiff else 12) + fmt_size
1020 )
1021 stripoffsets = None
1023 # pass 1: convert tags to binary format
1024 # always write tags in ascending order
1025 for tag, value in sorted(self._tags_v2.items()):
1026 if tag == STRIPOFFSETS:
1027 stripoffsets = len(entries)
1028 typ = self.tagtype[tag]
1029 logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
1030 is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
1031 if is_ifd:
1032 if self._bigtiff and offset >= 2**32:
1033 # Offset is too large for a LONG
1034 # BigTIFF allows an 8-byte offset to the sub-IFD
1035 typ = TiffTags.LONG8
1036 ifd = ImageFileDirectory_v2(self._get_ifh(), group=tag)
1037 values = self._tags_v2[tag]
1038 for ifd_tag, ifd_value in values.items():
1039 ifd[ifd_tag] = ifd_value
1040 data = ifd.tobytes(offset)
1041 else:
1042 values = value if isinstance(value, tuple) else (value,)
1043 data = self._write_dispatch[typ](self, *values)
1045 tagname = TiffTags.lookup(tag, self.group).name
1046 typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
1047 msg = f"save: {tagname} ({tag}) - type: {typname} ({typ}) - value: "
1048 msg += f"<table: {len(data)} bytes>" if len(data) >= 16 else str(values)
1049 logger.debug(msg)
1051 # count is sum of lengths for string and arbitrary data
1052 if is_ifd:
1053 count = 1
1054 elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
1055 count = len(data)
1056 else:
1057 count = len(values)
1058 # figure out if data fits into the entry
1059 if len(data) <= fmt_size:
1060 value = data.ljust(fmt_size, b"\0")
1061 data = b""
1062 else:
1063 if is_ifd and typ == TiffTags.LONG and self._bigtiff:
1064 # The offset needs to be left-justified within a BigTIFF entry,
1065 # because only the first 4 bytes will be read from this LONG,
1066 # regardless of endianness
1067 value = self._pack("L", offset).ljust(fmt_size, b"\0")
1068 else:
1069 value = self._pack(fmt, offset)
1070 offset += (len(data) + 1) // 2 * 2 # pad to word
1071 entries.append((tag, typ, count, value, data))
1073 # update strip offset data to point beyond auxiliary data
1074 if stripoffsets is not None:
1075 tag, typ, count, value, data = entries[stripoffsets]
1076 size, handler = self._load_dispatch[typ]
1077 values = [
1078 val + offset for val in handler(self, data or value, self.legacy_api)
1079 ]
1080 if data:
1081 data = self._write_dispatch[typ](self, *values)
1082 else:
1083 # The offsets fit within the entry
1084 # May be one offset, or two LONG offsets in BigTIFF
1085 if (
1086 self._bigtiff
1087 and typ == TiffTags.LONG
1088 and count == 1
1089 and values[0] >= 2**32
1090 ):
1091 # If a single offset is too large for a LONG,
1092 # in BigTIFF there is room to change the entry to LONG8
1093 typ = TiffTags.LONG8
1094 value = self._write_dispatch[typ](self, *values).ljust(fmt_size, b"\0")
1095 entries[stripoffsets] = tag, typ, count, value, data
1097 # pass 2: write entries to file
1098 for tag, typ, count, value, data in entries:
1099 logger.debug("%s %s %s %s %s", tag, typ, count, repr(value), repr(data))
1100 result += self._pack(
1101 "HHQ8s" if self._bigtiff else "HHL4s", tag, typ, count, value
1102 )
1104 # -- overwrite here for multi-page --
1105 result += self._pack(fmt, 0) # end of entries
1107 # pass 3: write auxiliary data to file
1108 for tag, typ, count, value, data in entries:
1109 result += data
1110 if len(data) & 1:
1111 result += b"\0"
1113 return result
1115 def save(self, fp: IO[bytes]) -> int:
1116 if fp.tell() == 0: # skip TIFF header on subsequent pages
1117 fp.write(self._get_ifh())
1119 offset = fp.tell()
1120 result = self.tobytes(offset)
1121 fp.write(result)
1122 return offset + len(result)
1125ImageFileDirectory_v2._load_dispatch = _load_dispatch
1126ImageFileDirectory_v2._write_dispatch = _write_dispatch
1127for idx, name in TYPES.items():
1128 name = name.replace(" ", "_")
1129 setattr(ImageFileDirectory_v2, f"load_{name}", _load_dispatch[idx][1])
1130 setattr(ImageFileDirectory_v2, f"write_{name}", _write_dispatch[idx])
1131del _load_dispatch, _write_dispatch, idx, name
1134# Legacy ImageFileDirectory support.
1135class ImageFileDirectory_v1(ImageFileDirectory_v2):
1136 """This class represents the **legacy** interface to a TIFF tag directory.
1138 Exposes a dictionary interface of the tags in the directory::
1140 ifd = ImageFileDirectory_v1()
1141 ifd[key] = 'Some Data'
1142 ifd.tagtype[key] = TiffTags.ASCII
1143 print(ifd[key])
1144 ('Some Data',)
1146 Also contains a dictionary of tag types as read from the tiff image file,
1147 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
1149 Values are returned as a tuple.
1151 .. deprecated:: 3.0.0
1152 """
1154 def __init__(self, *args: Any, **kwargs: Any) -> None:
1155 super().__init__(*args, **kwargs)
1156 self._legacy_api = True
1158 tags = property(lambda self: self._tags_v1)
1159 tagdata = property(lambda self: self._tagdata)
1161 # defined in ImageFileDirectory_v2
1162 tagtype: dict[int, int]
1163 """Dictionary of tag types"""
1165 @classmethod
1166 def from_v2(cls, original: ImageFileDirectory_v2) -> ImageFileDirectory_v1:
1167 """Returns an
1168 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1169 instance with the same data as is contained in the original
1170 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1171 instance.
1173 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1175 """
1177 ifd = cls(prefix=original.prefix)
1178 ifd._tagdata = original._tagdata
1179 ifd.tagtype = original.tagtype
1180 ifd.next = original.next # an indicator for multipage tiffs
1181 return ifd
1183 def to_v2(self) -> ImageFileDirectory_v2:
1184 """Returns an
1185 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1186 instance with the same data as is contained in the original
1187 :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1188 instance.
1190 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1192 """
1194 ifd = ImageFileDirectory_v2(prefix=self.prefix)
1195 ifd._tagdata = dict(self._tagdata)
1196 ifd.tagtype = dict(self.tagtype)
1197 ifd._tags_v2 = dict(self._tags_v2)
1198 return ifd
1200 def __contains__(self, tag: object) -> bool:
1201 return tag in self._tags_v1 or tag in self._tagdata
1203 def __len__(self) -> int:
1204 return len(set(self._tagdata) | set(self._tags_v1))
1206 def __iter__(self) -> Iterator[int]:
1207 return iter(set(self._tagdata) | set(self._tags_v1))
1209 def __setitem__(self, tag: int, value: Any) -> None:
1210 for legacy_api in (False, True):
1211 self._setitem(tag, value, legacy_api)
1213 def __getitem__(self, tag: int) -> Any:
1214 if tag not in self._tags_v1: # unpack on the fly
1215 data = self._tagdata[tag]
1216 typ = self.tagtype[tag]
1217 size, handler = self._load_dispatch[typ]
1218 for legacy in (False, True):
1219 self._setitem(tag, handler(self, data, legacy), legacy)
1220 val = self._tags_v1[tag]
1221 if not isinstance(val, (tuple, bytes)):
1222 val = (val,)
1223 return val
1226# undone -- switch this pointer
1227ImageFileDirectory = ImageFileDirectory_v1
1230##
1231# Image plugin for TIFF files.
1234class TiffImageFile(ImageFile.ImageFile):
1235 format = "TIFF"
1236 format_description = "Adobe TIFF"
1237 _close_exclusive_fp_after_loading = False
1239 def __init__(
1240 self,
1241 fp: StrOrBytesPath | IO[bytes],
1242 filename: str | bytes | None = None,
1243 ) -> None:
1244 self.tag_v2: ImageFileDirectory_v2
1245 """ Image file directory (tag dictionary) """
1247 self.tag: ImageFileDirectory_v1
1248 """ Legacy tag entries """
1250 self._n_frames: int | None = None
1251 super().__init__(fp, filename)
1253 def _open(self) -> None:
1254 """Open the first image in a TIFF file"""
1256 # Header
1257 assert self.fp is not None
1258 ifh = self.fp.read(8)
1259 if 43 in ifh[2:4]:
1260 ifh += self.fp.read(8)
1262 self.tag_v2 = ImageFileDirectory_v2(ifh)
1264 # setup frame pointers
1265 self.__first = self.__next = self.tag_v2.next
1266 self.__frame = -1
1267 self._fp = self.fp
1268 self._frame_pos: list[int] = []
1270 # Out-of-line tag data is budgeted across all frames, so that many
1271 # frames cannot each read the same data. Each frame's limit is fixed
1272 # when it is first loaded, so that reloading it gives the same tags.
1273 self._tag_data_remaining = self._get_fp_length()
1274 self._tag_data_limits: list[int] = []
1276 logger.debug("*** TiffImageFile._open ***")
1277 logger.debug("- __first: %s", self.__first)
1278 logger.debug("- ifh: %s", repr(ifh)) # Use repr to avoid str(bytes)
1280 # and load the first frame
1281 self._seek(0)
1283 @property
1284 def n_frames(self) -> int:
1285 current_n_frames = self._n_frames
1286 if current_n_frames is None:
1287 current = self.tell()
1288 self._seek(len(self._frame_pos))
1289 while self._n_frames is None:
1290 self._seek(self.tell() + 1)
1291 self.seek(current)
1292 assert self._n_frames is not None
1293 return self._n_frames
1295 def seek(self, frame: int) -> None:
1296 """Select a given frame as current image"""
1297 if not self._seek_check(frame):
1298 return
1299 self._seek(frame)
1300 if self._im is not None and (
1301 self.im.size != self._tile_size
1302 or self.im.mode != self.mode
1303 or self.readonly
1304 ):
1305 self._im = None
1307 def _seek(self, frame: int) -> None:
1308 if isinstance(self._fp, DeferredError):
1309 raise self._fp.ex
1310 self.fp = self._fp
1312 while len(self._frame_pos) <= frame:
1313 if not self.__next:
1314 msg = "no more images in TIFF file"
1315 raise EOFError(msg)
1316 logger.debug(
1317 "Seeking to frame %s, on frame %s, __next %s, location: %s",
1318 frame,
1319 self.__frame,
1320 self.__next,
1321 self.fp.tell(),
1322 )
1323 if self.__next >= 2**63:
1324 msg = "Unable to seek to frame"
1325 raise ValueError(msg)
1326 self.fp.seek(self.__next)
1327 self._frame_pos.append(self.__next)
1328 self._tag_data_limits.append(self._tag_data_remaining)
1329 logger.debug("Loading tags, location: %s", self.fp.tell())
1330 self.tag_v2._tag_data_limit = self._tag_data_remaining
1331 self.tag_v2.load(self.fp)
1332 self._tag_data_remaining -= self.tag_v2._tag_data_size
1333 if self.tag_v2.next in self._frame_pos:
1334 # This IFD has already been processed
1335 # Declare this to be the end of the image
1336 self.__next = 0
1337 else:
1338 self.__next = self.tag_v2.next
1339 if self.__next == 0 and self._n_frames is None:
1340 self._n_frames = frame + 1
1341 if not hasattr(self, "is_animated"):
1342 self.is_animated = self.__next != 0
1343 self.__frame += 1
1344 self.fp.seek(self._frame_pos[frame])
1345 self.tag_v2._tag_data_limit = self._tag_data_limits[frame]
1346 self.tag_v2.load(self.fp)
1347 if XMP in self.tag_v2:
1348 xmp = self.tag_v2[XMP]
1349 if isinstance(xmp, tuple) and len(xmp) == 1:
1350 xmp = xmp[0]
1351 self.info["xmp"] = xmp
1352 elif "xmp" in self.info:
1353 del self.info["xmp"]
1354 self._reload_exif()
1355 # fill the legacy tag/ifd entries
1356 self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
1357 self.__frame = frame
1358 self._setup()
1360 def tell(self) -> int:
1361 """Return the current frame number"""
1362 return self.__frame
1364 def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]:
1365 """
1366 Returns a dictionary of Photoshop "Image Resource Blocks".
1367 The keys are the image resource ID. For more information, see
1368 https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727
1370 :returns: Photoshop "Image Resource Blocks" in a dictionary.
1371 """
1372 blocks = {}
1373 val = self.tag_v2.get(ExifTags.Base.ImageResources)
1374 if val:
1375 while val.startswith(b"8BIM") and len(val) >= 12:
1376 id = i16(val[4:6])
1377 n = math.ceil((val[6] + 1) / 2) * 2
1378 try:
1379 size = i32(val[6 + n : 10 + n])
1380 except struct.error:
1381 break
1382 data = val[10 + n : 10 + n + size]
1383 blocks[id] = {"data": data}
1385 val = val[math.ceil((10 + n + size) / 2) * 2 :]
1386 return blocks
1388 def load(self) -> Image.core.PixelAccess | None:
1389 if not self.tile:
1390 return super().load()
1392 original_size = self._size
1393 self._size = self._tile_size
1395 try:
1396 if self.use_load_libtiff:
1397 self._load_libtiff()
1398 px = Image.Image.load(self)
1399 else:
1400 px = super().load()
1401 finally:
1402 self._size = original_size
1403 return px
1405 def load_end(self) -> None:
1406 # allow closing if we're on the first frame, there's no next
1407 # This is the ImageFile.load path only, libtiff specific below.
1408 if not self.is_animated:
1409 self._close_exclusive_fp_after_loading = True
1411 # load IFD data from fp before it is closed
1412 exif = self.getexif()
1413 for key in TiffTags.TAGS_V2_GROUPS:
1414 if key not in exif:
1415 continue
1416 exif.get_ifd(key)
1418 ImageOps.exif_transpose(self, in_place=True)
1419 if ExifTags.Base.Orientation in self.tag_v2:
1420 del self.tag_v2[ExifTags.Base.Orientation]
1422 def _load_libtiff(self) -> None:
1423 """Overload method triggered when we detect a compressed tiff
1424 Calls out to libtiff"""
1426 if not len(self.tile) == 1:
1427 msg = "Not exactly one tile"
1428 raise OSError(msg)
1430 self.load_prepare()
1432 extents = self.tile[0].extents
1433 args = self.tile[0].args
1435 # To be nice on memory footprint, if there's a
1436 # file descriptor, use that instead of reading
1437 # into a string in python.
1438 assert self.fp is not None
1439 try:
1440 fp = hasattr(self.fp, "fileno") and self.fp.fileno()
1441 # flush the file descriptor, prevents error on pypy 2.4+
1442 # should also eliminate the need for fp.tell
1443 # in _seek
1444 if hasattr(self.fp, "flush"):
1445 self.fp.flush()
1446 except OSError:
1447 # io.BytesIO have a fileno, but returns an OSError if
1448 # it doesn't use a file descriptor.
1449 fp = False
1451 if fp:
1452 assert isinstance(args, tuple)
1453 args_list = list(args)
1454 args_list[1] = fp
1455 args = tuple(args_list)
1457 decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig)
1458 try:
1459 decoder.setimage(self.im, extents)
1460 except ValueError as e:
1461 msg = "Couldn't set the image"
1462 raise OSError(msg) from e
1464 close_self_fp = self._exclusive_fp and not self.is_animated
1465 if hasattr(self.fp, "getvalue"):
1466 # We've got a stringio like thing passed in. Yay for all in memory.
1467 # The decoder needs the entire file in one shot, so there's not
1468 # a lot we can do here other than give it the entire file.
1469 # unless we could do something like get the address of the
1470 # underlying string for stringio.
1471 #
1472 # Rearranging for supporting byteio items, since they have a fileno
1473 # that returns an OSError if there's no underlying fp. Easier to
1474 # deal with here by reordering.
1475 logger.debug("have getvalue. just sending in a string from getvalue")
1476 n, err = decoder.decode(self.fp.getvalue())
1477 elif fp:
1478 # we've got a actual file on disk, pass in the fp.
1479 logger.debug("have fileno, calling fileno version of the decoder.")
1480 if not close_self_fp:
1481 self.fp.seek(0)
1482 # Save and restore the file position, because libtiff will move it
1483 # outside of the Python runtime, and that will confuse
1484 # io.BufferedReader and possible others.
1485 # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(),
1486 # because the buffer read head already may not equal the actual
1487 # file position, and fp.seek() may just adjust it's internal
1488 # pointer and not actually seek the OS file handle.
1489 pos = os.lseek(fp, 0, os.SEEK_CUR)
1490 # 4 bytes, otherwise the trace might error out
1491 n, err = decoder.decode(b"fpfp")
1492 os.lseek(fp, pos, os.SEEK_SET)
1493 else:
1494 # we have something else.
1495 logger.debug("don't have fileno or getvalue. just reading")
1496 self.fp.seek(0)
1497 # UNDONE -- so much for that buffer size thing.
1498 n, err = decoder.decode(self.fp.read())
1500 self.tile = []
1501 self.readonly = 0
1503 self.load_end()
1505 if close_self_fp:
1506 self.fp.close()
1507 self.fp = None # might be shared
1509 if err < 0:
1510 msg = f"decoder error {err}"
1511 raise OSError(msg)
1513 def _setup(self) -> None:
1514 """Setup this image object based on current tags"""
1516 if 0xBC01 in self.tag_v2:
1517 msg = "Windows Media Photo files not yet supported"
1518 raise OSError(msg)
1520 # extract relevant tags
1521 self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
1522 self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)
1524 # photometric is a required tag, but not everyone is reading
1525 # the specification
1526 photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
1528 # old style jpeg compression images most certainly are YCbCr
1529 if self._compression == "tiff_jpeg":
1530 photo = 6
1532 fillorder = self.tag_v2.get(FILLORDER, 1)
1534 logger.debug("*** Summary ***")
1535 logger.debug("- compression: %s", self._compression)
1536 logger.debug("- photometric_interpretation: %s", photo)
1537 logger.debug("- planar_configuration: %s", self._planar_configuration)
1538 logger.debug("- fill_order: %s", fillorder)
1539 logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
1541 # size
1542 try:
1543 xsize = self.tag_v2[IMAGEWIDTH]
1544 ysize = self.tag_v2[IMAGELENGTH]
1545 except KeyError as e:
1546 msg = "Missing dimensions"
1547 raise TypeError(msg) from e
1548 if not isinstance(xsize, int) or not isinstance(ysize, int):
1549 msg = "Invalid dimensions"
1550 raise ValueError(msg)
1551 self._tile_size = xsize, ysize
1552 Image._decompression_bomb_check(self._tile_size)
1553 orientation = self.tag_v2.get(ExifTags.Base.Orientation)
1554 if orientation in (5, 6, 7, 8):
1555 self._size = ysize, xsize
1556 else:
1557 self._size = xsize, ysize
1559 logger.debug("- size: %s", self.size)
1561 sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
1562 if len(sample_format) > 1 and max(sample_format) == min(sample_format):
1563 # SAMPLEFORMAT is properly per band, so an RGB image will
1564 # be (1,1,1). But, we don't support per band pixel types,
1565 # and anything more than one band is a uint8. So, just
1566 # take the first element. Revisit this if adding support
1567 # for more exotic images.
1568 sample_format = (sample_format[0],)
1570 bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
1571 extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
1572 samples_per_pixel = self.tag_v2.get(
1573 SAMPLESPERPIXEL,
1574 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
1575 )
1576 if photo in (2, 6, 8): # RGB, YCbCr, LAB
1577 bps_count = 3
1578 elif photo == 5: # CMYK
1579 bps_count = 4
1580 else:
1581 bps_count = 1
1582 if self._planar_configuration == 2 and extra_tuple and max(extra_tuple) == 0:
1583 # If components are stored separately,
1584 # then unspecified extra components at the end can be ignored
1585 bps_tuple = bps_tuple[: -len(extra_tuple)]
1586 samples_per_pixel -= len(extra_tuple)
1587 extra_tuple = ()
1588 bps_count += len(extra_tuple)
1589 bps_actual_count = len(bps_tuple)
1591 if samples_per_pixel > MAX_SAMPLESPERPIXEL:
1592 # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
1593 logger.error(
1594 "More samples per pixel than can be decoded: %s", samples_per_pixel
1595 )
1596 msg = "Invalid value for samples per pixel"
1597 raise SyntaxError(msg)
1599 if samples_per_pixel < bps_actual_count:
1600 # If a file has more values in bps_tuple than expected,
1601 # remove the excess.
1602 bps_tuple = bps_tuple[:samples_per_pixel]
1603 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
1604 # If a file has only one value in bps_tuple, when it should have more,
1605 # presume it is the same number of bits for all of the samples.
1606 bps_tuple = bps_tuple * samples_per_pixel
1608 if len(bps_tuple) != samples_per_pixel:
1609 msg = "unknown data organization"
1610 raise SyntaxError(msg)
1612 # mode: check photometric interpretation and bits per pixel
1613 key = (
1614 self.tag_v2.prefix,
1615 photo,
1616 sample_format,
1617 fillorder,
1618 bps_tuple,
1619 extra_tuple,
1620 )
1621 logger.debug("format key: %s", key)
1622 try:
1623 self._mode, rawmode = OPEN_INFO[key]
1624 except KeyError as e:
1625 logger.debug("- unsupported format")
1626 msg = "unknown pixel mode"
1627 raise SyntaxError(msg) from e
1629 logger.debug("- raw mode: %s", rawmode)
1630 logger.debug("- pil mode: %s", self.mode)
1632 self.info["compression"] = self._compression
1634 # Clear info from previous frame
1635 self.info.pop("dpi", None)
1636 self.info.pop("resolution", None)
1638 xres = self.tag_v2.get(X_RESOLUTION)
1639 yres = self.tag_v2.get(Y_RESOLUTION)
1641 if xres and yres:
1642 resunit = self.tag_v2.get(RESOLUTION_UNIT)
1643 if resunit == 2: # dots per inch
1644 self.info["dpi"] = (xres, yres)
1645 elif resunit == 3: # dots per centimeter. convert to dpi
1646 self.info["dpi"] = (xres * 2.54, yres * 2.54)
1647 elif resunit is None: # used to default to 1, but now 2)
1648 self.info["dpi"] = (xres, yres)
1649 # For backward compatibility,
1650 # we also preserve the old behavior
1651 self.info["resolution"] = xres, yres
1652 else: # No absolute unit of measurement
1653 self.info["resolution"] = xres, yres
1655 # build tile descriptors
1656 x = y = layer = 0
1657 self.tile = []
1658 self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
1659 if self.use_load_libtiff:
1660 # Decoder expects entire file as one tile.
1661 # There's a buffer size limit in load (64k)
1662 # so large g4 images will fail if we use that
1663 # function.
1664 #
1665 # Setup the one tile for the whole image, then
1666 # use the _load_libtiff function.
1668 # libtiff handles the fillmode for us, so 1;IR should
1669 # actually be 1;I. Including the R double reverses the
1670 # bits, so stripes of the image are reversed. See
1671 # https://github.com/python-pillow/Pillow/issues/279
1672 if fillorder == 2:
1673 # Replace fillorder with fillorder=1
1674 key = key[:3] + (1,) + key[4:]
1675 logger.debug("format key: %s", key)
1676 # this should always work, since all the
1677 # fillorder==2 modes have a corresponding
1678 # fillorder=1 mode
1679 self._mode, rawmode = OPEN_INFO[key]
1680 if photo == 6:
1681 self._mode = "RGB"
1682 if self._compression in "jpeg" and self._planar_configuration == 1:
1683 # YCbCr images with new jpeg compression with pixels in one plane
1684 # unpacked straight into RGB values
1685 rawmode = "RGB"
1686 else:
1687 rawmode = "RGBX"
1688 # libtiff always returns the bytes in native order.
1689 # we're expecting image byte order. So, if the rawmode
1690 # contains I;16, we need to convert from native to image
1691 # byte order.
1692 elif rawmode == "I;16":
1693 rawmode = "I;16N"
1694 elif rawmode.endswith((";16B", ";16L")):
1695 rawmode = rawmode[:-1] + "N"
1697 # Offset in the tile tuple is 0, we go from 0,0 to
1698 # w,h, and we only do this once -- eds
1699 a = (rawmode, False, self.tag_v2.offset)
1700 self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a))
1702 elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
1703 # striped image
1704 if STRIPOFFSETS in self.tag_v2:
1705 offsets = self.tag_v2[STRIPOFFSETS]
1706 h = self.tag_v2.get(ROWSPERSTRIP, ysize)
1707 w = xsize
1708 else:
1709 # tiled image
1710 offsets = self.tag_v2[TILEOFFSETS]
1711 tilewidth = self.tag_v2.get(TILEWIDTH)
1712 h = self.tag_v2.get(TILELENGTH)
1713 if not isinstance(tilewidth, int) or not isinstance(h, int):
1714 msg = "Invalid tile dimensions"
1715 raise ValueError(msg)
1716 w = tilewidth
1718 if w == xsize and h == ysize and self._planar_configuration != 2:
1719 # Every tile covers the image. Only use the last offset
1720 offsets = offsets[-1:]
1722 for offset in offsets:
1723 if x + w > xsize:
1724 stride = w * sum(bps_tuple) / 8 # bytes per line
1725 else:
1726 stride = 0
1728 tile_rawmode = rawmode
1729 if self._planar_configuration == 2:
1730 # each band on it's own layer
1731 tile_rawmode = rawmode[layer]
1732 # adjust stride width accordingly
1733 stride /= bps_count
1735 args = (tile_rawmode, int(stride), 1)
1736 self.tile.append(
1737 ImageFile._Tile(
1738 self._compression,
1739 (x, y, min(x + w, xsize), min(y + h, ysize)),
1740 offset,
1741 args,
1742 )
1743 )
1744 x += w
1745 if x >= xsize:
1746 x, y = 0, y + h
1747 if y >= ysize:
1748 y = 0
1749 layer += 1
1750 else:
1751 logger.debug("- unsupported data organization")
1752 msg = "unknown data organization"
1753 raise SyntaxError(msg)
1755 # Fix up info.
1756 if ICCPROFILE in self.tag_v2:
1757 self.info["icc_profile"] = self.tag_v2[ICCPROFILE]
1759 # fixup palette descriptor
1761 if self.mode in ["P", "PA"]:
1762 palette = tuple(b // 256 for b in self.tag_v2[COLORMAP])
1763 self.palette = ImagePalette.raw("RGB;L", palette)
1764 else:
1765 self.palette = None
1768#
1769# --------------------------------------------------------------------
1770# Write TIFF files
1772# little endian is default except for image modes with
1773# explicit big endian byte-order
1775SAVE_INFO = {
1776 # mode => rawmode, byteorder, photometrics,
1777 # sampleformat, bitspersample, extra
1778 "1": ("1", II, 1, 1, (1,), None),
1779 "L": ("L", II, 1, 1, (8,), None),
1780 "LA": ("LA", II, 1, 1, (8, 8), 2),
1781 "P": ("P", II, 3, 1, (8,), None),
1782 "PA": ("PA", II, 3, 1, (8, 8), 2),
1783 "I": ("I;32S", II, 1, 2, (32,), None),
1784 "I;16": ("I;16", II, 1, 1, (16,), None),
1785 "I;16L": ("I;16L", II, 1, 1, (16,), None),
1786 "F": ("F;32F", II, 1, 3, (32,), None),
1787 "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
1788 "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
1789 "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
1790 "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
1791 "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
1792 "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
1793 "I;16B": ("I;16B", MM, 1, 1, (16,), None),
1794}
1797def _validate_tags(tags: dict[int, Any]) -> None:
1798 if COLORMAP in tags:
1799 bps = tags.get(BITSPERSAMPLE, 1)
1800 if isinstance(bps, tuple):
1801 bps = bps[0]
1802 expected = (1 << bps) * 3
1803 if len(tags[COLORMAP]) != expected:
1804 msg = f"Requiring {expected} items for Colormap"
1805 raise ValueError(msg)
1808def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
1809 try:
1810 rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
1811 except KeyError as e:
1812 msg = f"cannot write mode {im.mode} as TIFF"
1813 raise OSError(msg) from e
1815 encoderinfo = im.encoderinfo
1816 encoderconfig = im.encoderconfig
1818 ifd = ImageFileDirectory_v2(prefix=prefix)
1819 if encoderinfo.get("big_tiff"):
1820 ifd._bigtiff = True
1822 try:
1823 compression = encoderinfo["compression"]
1824 except KeyError:
1825 compression = im.info.get("compression")
1826 if isinstance(compression, int):
1827 # compression value may be from BMP. Ignore it
1828 compression = None
1829 if compression is None:
1830 compression = "raw"
1831 elif compression == "tiff_jpeg":
1832 # OJPEG is obsolete, so use new-style JPEG compression instead
1833 compression = "jpeg"
1834 elif compression == "tiff_deflate":
1835 compression = "tiff_adobe_deflate"
1837 libtiff = WRITE_LIBTIFF or compression != "raw"
1839 ifd[IMAGEWIDTH] = im.size[0]
1840 ifd[IMAGELENGTH] = im.size[1]
1842 # write any arbitrary tags passed in as an ImageFileDirectory
1843 subifds: list[int] = []
1844 if "tiffinfo" in encoderinfo:
1845 info = encoderinfo["tiffinfo"]
1846 elif "exif" in encoderinfo:
1847 info = encoderinfo["exif"]
1848 if isinstance(info, bytes):
1849 exif = Image.Exif()
1850 exif.load(info)
1851 info = exif
1852 elif isinstance(info, Image.Exif) and not libtiff:
1853 subifds.extend(
1854 key for key in TiffTags.TAGS_V2_GROUPS if info._ifds.get(key, {}) != {}
1855 )
1856 else:
1857 info = {}
1858 logger.debug("Tiffinfo Keys: %s", list(info))
1859 if isinstance(info, ImageFileDirectory_v1):
1860 info = info.to_v2()
1861 for key in {*info, *subifds}:
1862 if libtiff and key in {IMAGEWIDTH, IMAGELENGTH}:
1863 continue
1864 if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS:
1865 ifd[key] = info.get_ifd(key)
1866 else:
1867 ifd[key] = info.get(key)
1868 try:
1869 ifd.tagtype[key] = info.tagtype[key]
1870 except Exception:
1871 pass # might not be an IFD. Might not have populated type
1873 # required for color libtiff images
1874 ifd[PLANAR_CONFIGURATION] = 1
1876 legacy_ifd = {}
1877 if hasattr(im, "tag"):
1878 legacy_ifd = im.tag.to_v2()
1880 supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
1881 if supplied_tags.get(PLANAR_CONFIGURATION) == 2 and EXTRASAMPLES in supplied_tags:
1882 # If the image used separate component planes,
1883 # then EXTRASAMPLES should be ignored when saving contiguously
1884 if SAMPLESPERPIXEL in supplied_tags:
1885 supplied_tags[SAMPLESPERPIXEL] -= len(supplied_tags[EXTRASAMPLES])
1886 del supplied_tags[EXTRASAMPLES]
1887 for tag in (
1888 # IFD offset that may not be correct in the saved image
1889 EXIFIFD,
1890 # Determined by the image format and should not be copied from legacy_ifd.
1891 SAMPLEFORMAT,
1892 ):
1893 if tag in supplied_tags:
1894 del supplied_tags[tag]
1896 # additions written by Greg Couch, gregc@cgl.ucsf.edu
1897 # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
1898 if hasattr(im, "tag_v2"):
1899 # preserve tags from original TIFF image file
1900 for key in (
1901 RESOLUTION_UNIT,
1902 X_RESOLUTION,
1903 Y_RESOLUTION,
1904 IPTC_NAA_CHUNK,
1905 PHOTOSHOP_CHUNK,
1906 XMP,
1907 ):
1908 if key in im.tag_v2:
1909 if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in (
1910 TiffTags.BYTE,
1911 TiffTags.UNDEFINED,
1912 ):
1913 del supplied_tags[key]
1914 else:
1915 ifd[key] = im.tag_v2[key]
1916 ifd.tagtype[key] = im.tag_v2.tagtype[key]
1918 # preserve ICC profile (should also work when saving other formats
1919 # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
1920 icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
1921 if icc:
1922 ifd[ICCPROFILE] = icc
1924 for key, name in [
1925 (IMAGEDESCRIPTION, "description"),
1926 (X_RESOLUTION, "resolution"),
1927 (Y_RESOLUTION, "resolution"),
1928 (X_RESOLUTION, "x_resolution"),
1929 (Y_RESOLUTION, "y_resolution"),
1930 (RESOLUTION_UNIT, "resolution_unit"),
1931 (SOFTWARE, "software"),
1932 (DATE_TIME, "date_time"),
1933 (ARTIST, "artist"),
1934 (COPYRIGHT, "copyright"),
1935 ]:
1936 if name in encoderinfo:
1937 ifd[key] = encoderinfo[name]
1939 dpi = encoderinfo.get("dpi")
1940 if dpi:
1941 ifd[RESOLUTION_UNIT] = 2
1942 ifd[X_RESOLUTION] = dpi[0]
1943 ifd[Y_RESOLUTION] = dpi[1]
1945 if bits != (1,):
1946 ifd[BITSPERSAMPLE] = bits
1947 ifd[SAMPLESPERPIXEL] = len(bits)
1948 if extra is not None:
1949 ifd[EXTRASAMPLES] = extra
1950 if format != 1:
1951 ifd[SAMPLEFORMAT] = format
1953 if PHOTOMETRIC_INTERPRETATION not in ifd:
1954 ifd[PHOTOMETRIC_INTERPRETATION] = photo
1955 elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
1956 if im.mode == "1":
1957 inverted_im = im.copy()
1958 px = inverted_im.load()
1959 if px is not None:
1960 for y in range(inverted_im.height):
1961 for x in range(inverted_im.width):
1962 px[x, y] = 0 if px[x, y] == 255 else 255
1963 im = inverted_im
1964 else:
1965 im = ImageOps.invert(im)
1967 if im.mode in ["P", "PA"]:
1968 lut = im.im.getpalette("RGB;L")
1969 colormap = []
1970 colors = len(lut) // 3
1971 for i in range(3):
1972 # Multiplying by 257 scales 0-255 to 0-65535
1973 colormap += [v * 257 for v in lut[colors * i : colors * (i + 1)]]
1974 colormap += [0] * (256 - colors)
1975 ifd[COLORMAP] = colormap
1976 # data orientation
1977 w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
1978 stride = len(bits) * ((w * bits[0] + 7) // 8)
1979 if ROWSPERSTRIP not in ifd:
1980 # aim for given strip size (64 KB by default) when using libtiff writer
1981 if libtiff:
1982 im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
1983 rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
1984 # JPEG encoder expects multiple of 8 rows
1985 if compression == "jpeg":
1986 rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
1987 else:
1988 rows_per_strip = h
1989 if rows_per_strip == 0:
1990 rows_per_strip = 1
1991 ifd[ROWSPERSTRIP] = rows_per_strip
1992 strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
1993 strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
1994 if strip_byte_counts >= 2**16:
1995 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
1996 ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
1997 stride * h - strip_byte_counts * (strips_per_image - 1),
1998 )
1999 ifd[STRIPOFFSETS] = tuple(
2000 range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
2001 ) # this is adjusted by IFD writer
2002 # no compression by default:
2003 ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
2005 if im.mode == "YCbCr":
2006 for tag, default_value in {
2007 YCBCRSUBSAMPLING: (1, 1),
2008 REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
2009 }.items():
2010 ifd.setdefault(tag, default_value)
2012 blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
2013 if libtiff:
2014 if "quality" in encoderinfo:
2015 quality = encoderinfo["quality"]
2016 if not isinstance(quality, int) or quality < 0 or quality > 100:
2017 msg = "Invalid quality setting"
2018 raise ValueError(msg)
2019 if compression != "jpeg":
2020 msg = "quality setting only supported for 'jpeg' compression"
2021 raise ValueError(msg)
2022 ifd[JPEGQUALITY] = quality
2024 logger.debug("Saving using libtiff encoder")
2025 logger.debug("Items: %s", sorted(ifd.items()))
2026 _fp = 0
2027 if hasattr(fp, "fileno"):
2028 try:
2029 fp.seek(0)
2030 _fp = fp.fileno()
2031 except io.UnsupportedOperation:
2032 pass
2034 # optional types for non core tags
2035 types = {}
2036 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
2037 # based on the data in the strip.
2038 # OSUBFILETYPE is deprecated.
2039 # TRANSFERFUNCTION is not supported by the libtiff encoder.
2040 # SUBIFD may also cause a segfault.
2041 blocklist += [
2042 OSUBFILETYPE,
2043 STRIPBYTECOUNTS,
2044 STRIPOFFSETS,
2045 TRANSFERFUNCTION,
2046 SUBIFD,
2047 ]
2049 # bits per sample is a single short in the tiff directory, not a list.
2050 atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
2051 # Merge the ones that we have with (optional) more bits from
2052 # the original file, e.g x,y resolution so that we can
2053 # save(load('')) == original file.
2054 for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
2055 # Libtiff can only process certain core items without adding
2056 # them to the custom dictionary.
2057 # Custom items are supported for int, float, unicode, string and byte
2058 # values. Other types and tuples require a tagtype.
2059 if tag not in TiffTags.LIBTIFF_CORE:
2060 if tag in TiffTags.TAGS_V2_GROUPS:
2061 types[tag] = TiffTags.LONG8
2062 elif tag in ifd.tagtype:
2063 types[tag] = ifd.tagtype[tag]
2064 elif isinstance(value, (int, float, str, bytes)) or (
2065 isinstance(value, tuple)
2066 and all(isinstance(v, (int, float, IFDRational)) for v in value)
2067 ):
2068 type = TiffTags.lookup(tag).type
2069 if type:
2070 types[tag] = type
2071 if tag not in atts and tag not in blocklist:
2072 if isinstance(value, str):
2073 atts[tag] = value.encode("ascii", "replace") + b"\0"
2074 elif isinstance(value, IFDRational):
2075 atts[tag] = float(value)
2076 else:
2077 atts[tag] = value
2079 if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
2080 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
2081 _validate_tags(atts)
2083 logger.debug("Converted items: %s", sorted(atts.items()))
2085 # libtiff always expects the bytes in native order.
2086 # we're storing image byte order. So, if the rawmode
2087 # contains I;16, we need to convert from native to image
2088 # byte order.
2089 if im.mode in ("I;16", "I;16B", "I;16L"):
2090 rawmode = "I;16N"
2092 # Pass tags as sorted list so that the tags are set in a fixed order.
2093 # This is required by libtiff for some tags. For example, the JPEGQUALITY
2094 # pseudo tag requires that the COMPRESS tag was already set.
2095 tags = list(atts.items())
2096 tags.sort()
2097 a = (rawmode, _fp, filename, tags, types)
2098 encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
2099 try:
2100 encoder.setimage(im.im)
2101 while True:
2102 errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:]
2103 if not _fp:
2104 fp.write(data)
2105 if errcode:
2106 break
2107 if errcode < 0:
2108 msg = f"encoder error {errcode} when writing image file"
2109 raise OSError(msg)
2110 finally:
2111 encoder.cleanup()
2113 else:
2114 for tag in blocklist:
2115 del ifd[tag]
2116 _validate_tags(ifd._tags_v2)
2117 offset = ifd.save(fp)
2119 ImageFile._save(
2120 im,
2121 fp,
2122 [ImageFile._Tile("raw", None, offset, (rawmode, stride, 1))],
2123 )
2125 # -- helper for multi-page save --
2126 if "_debug_multipage" in encoderinfo:
2127 # just to access o32 and o16 (using correct byte order)
2128 setattr(im, "_debug_multipage", ifd)
2131class AppendingTiffWriter(io.BytesIO):
2132 fieldSizes = [
2133 0, # None
2134 1, # byte
2135 1, # ascii
2136 2, # short
2137 4, # long
2138 8, # rational
2139 1, # sbyte
2140 1, # undefined
2141 2, # sshort
2142 4, # slong
2143 8, # srational
2144 4, # float
2145 8, # double
2146 4, # ifd
2147 2, # unicode
2148 4, # complex
2149 8, # long8
2150 ]
2152 Tags = {
2153 273, # StripOffsets
2154 288, # FreeOffsets
2155 324, # TileOffsets
2156 519, # JPEGQTables
2157 520, # JPEGDCTables
2158 521, # JPEGACTables
2159 }
2161 def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None:
2162 self.f: IO[bytes]
2163 if is_path(fn):
2164 self.name = fn
2165 self.close_fp = True
2166 try:
2167 self.f = open(fn, "w+b" if new else "r+b")
2168 except OSError:
2169 self.f = open(fn, "w+b")
2170 else:
2171 self.f = cast("IO[bytes]", fn)
2172 self.close_fp = False
2173 self.beginning = self.f.tell()
2174 self.setup()
2176 def setup(self) -> None:
2177 # Reset everything.
2178 self.f.seek(self.beginning, os.SEEK_SET)
2180 self.whereToWriteNewIFDOffset: int | None = None
2181 self.offsetOfNewPage = 0
2183 self.IIMM = iimm = self.f.read(4)
2184 self._bigtiff = b"\x2b" in iimm
2185 if not iimm:
2186 # empty file - first page
2187 self.isFirst = True
2188 return
2190 self.isFirst = False
2191 if iimm not in PREFIXES:
2192 msg = "Invalid TIFF file header"
2193 raise RuntimeError(msg)
2195 self.setEndian("<" if iimm.startswith(II) else ">")
2197 if self._bigtiff:
2198 self.f.seek(4, os.SEEK_CUR)
2199 self.skipIFDs()
2200 self.goToEnd()
2202 def finalize(self) -> None:
2203 if self.isFirst:
2204 return
2206 # fix offsets
2207 self.f.seek(self.offsetOfNewPage)
2209 iimm = self.f.read(4)
2210 if not iimm:
2211 # Make it easy to finish a frame without committing to a new one.
2212 return
2214 if iimm != self.IIMM:
2215 msg = "IIMM of new page doesn't match IIMM of first page"
2216 raise RuntimeError(msg)
2218 if self._bigtiff:
2219 self.f.seek(4, os.SEEK_CUR)
2220 ifd_offset = self._read(8 if self._bigtiff else 4)
2221 ifd_offset += self.offsetOfNewPage
2222 assert self.whereToWriteNewIFDOffset is not None
2223 self.f.seek(self.whereToWriteNewIFDOffset)
2224 self._write(ifd_offset, 8 if self._bigtiff else 4)
2225 self.f.seek(ifd_offset)
2226 self.fixIFD()
2228 def newFrame(self) -> None:
2229 # Call this to finish a frame.
2230 self.finalize()
2231 self.setup()
2233 def __enter__(self) -> Self:
2234 return self
2236 def __exit__(self, *args: object) -> None:
2237 if self.close_fp:
2238 self.close()
2240 def tell(self) -> int:
2241 return self.f.tell() - self.offsetOfNewPage
2243 def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
2244 """
2245 :param offset: Distance to seek.
2246 :param whence: Whether the distance is relative to the start,
2247 end or current position.
2248 :returns: The resulting position, relative to the start.
2249 """
2250 if whence == os.SEEK_SET:
2251 offset += self.offsetOfNewPage
2253 self.f.seek(offset, whence)
2254 return self.tell()
2256 def goToEnd(self) -> None:
2257 self.f.seek(0, os.SEEK_END)
2258 pos = self.f.tell()
2260 # pad to 16 byte boundary
2261 pad_bytes = 16 - pos % 16
2262 if 0 < pad_bytes < 16:
2263 self.f.write(bytes(pad_bytes))
2264 self.offsetOfNewPage = self.f.tell()
2266 def setEndian(self, endian: str) -> None:
2267 self.endian = endian
2268 self.longFmt = f"{self.endian}L"
2269 self.shortFmt = f"{self.endian}H"
2270 self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
2272 def skipIFDs(self) -> None:
2273 while True:
2274 ifd_offset = self._read(8 if self._bigtiff else 4)
2275 if ifd_offset == 0:
2276 self.whereToWriteNewIFDOffset = self.f.tell() - (
2277 8 if self._bigtiff else 4
2278 )
2279 break
2281 self.f.seek(ifd_offset)
2282 num_tags = self._read(8 if self._bigtiff else 2)
2283 self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
2285 def write(self, data: Buffer, /) -> int:
2286 return self.f.write(data)
2288 def _fmt(self, field_size: int) -> str:
2289 try:
2290 return {2: "H", 4: "L", 8: "Q"}[field_size]
2291 except KeyError:
2292 msg = "offset is not supported"
2293 raise RuntimeError(msg)
2295 def _read(self, field_size: int) -> int:
2296 (value,) = struct.unpack(
2297 self.endian + self._fmt(field_size), self.f.read(field_size)
2298 )
2299 return value
2301 def readShort(self) -> int:
2302 return self._read(2)
2304 def readLong(self) -> int:
2305 return self._read(4)
2307 @staticmethod
2308 def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
2309 if bytes_written is not None and bytes_written != expected:
2310 msg = f"wrote only {bytes_written} bytes but wanted {expected}"
2311 raise RuntimeError(msg)
2313 def _rewriteLast(
2314 self, value: int, field_size: int, new_field_size: int = 0
2315 ) -> None:
2316 self.f.seek(-field_size, os.SEEK_CUR)
2317 if not new_field_size:
2318 new_field_size = field_size
2319 bytes_written = self.f.write(
2320 struct.pack(self.endian + self._fmt(new_field_size), value)
2321 )
2322 self._verify_bytes_written(bytes_written, new_field_size)
2324 def rewriteLastShortToLong(self, value: int) -> None:
2325 self._rewriteLast(value, 2, 4)
2327 def rewriteLastShort(self, value: int) -> None:
2328 return self._rewriteLast(value, 2)
2330 def rewriteLastLong(self, value: int) -> None:
2331 return self._rewriteLast(value, 4)
2333 def _write(self, value: int, field_size: int) -> None:
2334 bytes_written = self.f.write(
2335 struct.pack(self.endian + self._fmt(field_size), value)
2336 )
2337 self._verify_bytes_written(bytes_written, field_size)
2339 def writeShort(self, value: int) -> None:
2340 self._write(value, 2)
2342 def writeLong(self, value: int) -> None:
2343 self._write(value, 4)
2345 def close(self) -> None:
2346 self.finalize()
2347 if self.close_fp:
2348 self.f.close()
2350 def fixIFD(self) -> None:
2351 num_tags = self._read(8 if self._bigtiff else 2)
2353 for i in range(num_tags):
2354 tag, field_type, count = struct.unpack(
2355 self.tagFormat, self.f.read(12 if self._bigtiff else 8)
2356 )
2358 field_size = self.fieldSizes[field_type]
2359 total_size = field_size * count
2360 fmt_size = 8 if self._bigtiff else 4
2361 is_local = total_size <= fmt_size
2362 if not is_local:
2363 offset = self._read(fmt_size) + self.offsetOfNewPage
2364 self._rewriteLast(offset, fmt_size)
2366 if tag in self.Tags:
2367 cur_pos = self.f.tell()
2369 logger.debug(
2370 "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
2371 TiffTags.lookup(tag).name,
2372 tag,
2373 TYPES.get(field_type, "unknown"),
2374 field_type,
2375 field_size,
2376 count,
2377 )
2379 if is_local:
2380 self._fixOffsets(count, field_size)
2381 self.f.seek(cur_pos + fmt_size)
2382 else:
2383 self.f.seek(offset)
2384 self._fixOffsets(count, field_size)
2385 self.f.seek(cur_pos)
2387 elif is_local:
2388 # skip the locally stored value that is not an offset
2389 self.f.seek(fmt_size, os.SEEK_CUR)
2391 def _fixOffsets(self, count: int, field_size: int) -> None:
2392 for i in range(count):
2393 offset = self._read(field_size)
2394 offset += self.offsetOfNewPage
2396 new_field_size = 0
2397 if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
2398 # offset is now too large - we must convert long to long8
2399 new_field_size = 8
2400 elif field_size == 2 and offset >= 2**16:
2401 # offset is now too large - we must convert short to long
2402 new_field_size = 4
2403 if new_field_size:
2404 if count != 1:
2405 msg = "not implemented"
2406 raise RuntimeError(msg) # XXX TODO
2408 # simple case - the offset is just one and therefore it is
2409 # local (not referenced with another offset)
2410 self._rewriteLast(offset, field_size, new_field_size)
2411 # Move back past the new offset, past 'count', and before 'field_type'
2412 rewind = -new_field_size - 4 - 2
2413 self.f.seek(rewind, os.SEEK_CUR)
2414 self.writeShort(new_field_size) # rewrite the type
2415 self.f.seek(2 - rewind, os.SEEK_CUR)
2416 else:
2417 self._rewriteLast(offset, field_size)
2419 def fixOffsets(
2420 self, count: int, isShort: bool = False, isLong: bool = False
2421 ) -> None:
2422 if isShort:
2423 field_size = 2
2424 elif isLong:
2425 field_size = 4
2426 else:
2427 field_size = 0
2428 return self._fixOffsets(count, field_size)
2431def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
2432 append_images = list(im.encoderinfo.get("append_images", []))
2433 if not hasattr(im, "n_frames") and not append_images:
2434 return _save(im, fp, filename)
2436 cur_idx = im.tell()
2437 try:
2438 with AppendingTiffWriter(fp) as tf:
2439 for ims in [im] + append_images:
2440 encoderinfo = ims._attach_default_encoderinfo(im)
2441 if not hasattr(ims, "encoderconfig"):
2442 ims.encoderconfig = ()
2443 nfr = getattr(ims, "n_frames", 1)
2445 for idx in range(nfr):
2446 ims.seek(idx)
2447 ims.load()
2448 _save(ims, tf, filename)
2449 tf.newFrame()
2450 ims.encoderinfo = encoderinfo
2451 finally:
2452 im.seek(cur_idx)
2455#
2456# --------------------------------------------------------------------
2457# Register
2459Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
2460Image.register_save(TiffImageFile.format, _save)
2461Image.register_save_all(TiffImageFile.format, _save_all)
2463Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
2465Image.register_mime(TiffImageFile.format, "image/tiff")