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
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
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
62TYPE_CHECKING = False
63if TYPE_CHECKING:
64 from collections.abc import Iterator
65 from typing import NoReturn
67 from ._typing import Buffer, IntegralLike, StrOrBytesPath
69logger = logging.getLogger(__name__)
71# Set these to true to force use of libtiff for reading or writing.
72READ_LIBTIFF = False
73WRITE_LIBTIFF = False
74STRIP_SIZE = 65536
76II = b"II" # little-endian (Intel style)
77MM = b"MM" # big-endian (Motorola style)
79#
80# --------------------------------------------------------------------
81# Read TIFF files
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
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
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}
149COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
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}
278MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO)
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]
290def _accept(prefix: bytes) -> bool:
291 return prefix.startswith(tuple(PREFIXES))
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
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
308 if min(float(i) for i in n_d) < min_val:
309 n_d = _limit_rational(val, abs(min_val))
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)
315 return n_d
318##
319# Wrapper for TIFF IFDs.
321_load_dispatch = {}
322_write_dispatch = {}
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)
331 return delegate
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.
338 e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
339 """
341 """ If the denominator is 0, store this as a float('nan'), otherwise store
342 as a fractions.Fraction(). Delegate as appropriate
344 """
346 __slots__ = ("_numerator", "_denominator", "_val")
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
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
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)
382 @property
383 def numerator(self) -> IntegralLike:
384 return self._numerator
386 @property
387 def denominator(self) -> int:
388 return self._denominator
390 def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]:
391 """
393 :param max_denominator: Integer, the maximum denominator value
394 :returns: Tuple of (numerator, denominator)
395 """
397 if self.denominator == 0:
398 return self.numerator, self.denominator
400 assert isinstance(self._val, Fraction)
401 f = self._val.limit_denominator(max_denominator)
402 return f.numerator, f.denominator
404 def __repr__(self) -> str:
405 return str(float(self._val))
407 def __hash__(self) -> int: # type: ignore[override]
408 return self._val.__hash__()
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
418 def __getstate__(self) -> list[float | Fraction | IntegralLike]:
419 return [self._val, self._numerator, self._denominator]
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
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 """
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__")
472_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any]
475def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]:
476 def decorator(func: _LoaderFunc) -> _LoaderFunc:
477 from .TiffTags import TYPES
479 if func.__name__.startswith("load_"):
480 TYPES[idx] = func.__name__[5:].replace("_", " ")
481 _load_dispatch[idx] = size, func # noqa: F821
482 return func
484 return decorator
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
492 return decorator
495def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None:
496 from .TiffTags import TYPES
498 idx, fmt, name = idx_fmt_name
499 TYPES[idx] = name
500 size = struct.calcsize(f"={fmt}")
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)
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 )
513if TYPE_CHECKING:
514 _IFDv2Base = MutableMapping[int, Any]
515else:
516 _IFDv2Base = MutableMapping
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.
523 Exposes a dictionary interface of the tags in the directory::
525 ifd = ImageFileDirectory_v2()
526 ifd[key] = 'Some Data'
527 ifd.tagtype[key] = TiffTags.ASCII
528 print(ifd[key])
529 'Some Data'
531 Individual values are returned as the strings or numbers, sequences are
532 returned as tuples of the values.
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.
540 Data Structures:
542 * ``self.tagtype = {}``
544 * Key: numerical TIFF tag number
545 * Value: integer corresponding to the data type from
546 :py:data:`.TiffTags.TYPES`
548 .. versionadded:: 3.0.0
550 'Internal' data structures:
552 * ``self._tags_v2 = {}``
554 * Key: numerical TIFF tag number
555 * Value: decoded data, as tuple for multiple values
557 * ``self._tagdata = {}``
559 * Key: numerical TIFF tag number
560 * Value: undecoded byte string from file
562 * ``self._tags_v1 = {}``
564 * Key: numerical TIFF tag number
565 * Value: decoded data in the v1 format
567 Tags will be found in the private attributes ``self._tagdata``, and in
568 ``self._tags_v2`` once decoded.
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``.
578 """
580 _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {}
581 _write_dispatch: dict[int, Callable[..., Any]] = {}
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.
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.
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
622 prefix = property(lambda self: self._prefix)
623 offset = property(lambda self: self._offset)
625 @property
626 def legacy_api(self) -> bool:
627 return self._legacy_api
629 @legacy_api.setter
630 def legacy_api(self, value: bool) -> NoReturn:
631 msg = "Not allowing setting of legacy api"
632 raise Exception(msg)
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
642 def __str__(self) -> str:
643 return str(dict(self))
645 def named(self) -> dict[str, Any]:
646 """
647 :returns: dict of name|key: value
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 }
656 def __len__(self) -> int:
657 return len(set(self._tagdata) | set(self._tags_v2))
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
670 def __contains__(self, tag: object) -> bool:
671 return tag in self._tags_v2 or tag in self._tagdata
673 def __setitem__(self, tag: int, value: Any) -> None:
674 self._setitem(tag, value, self.legacy_api)
676 def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None:
677 basetypes = (Number, bytes, str)
679 info = TiffTags.lookup(tag, self.group)
680 values = [value] if isinstance(value, basetypes) else value
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
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]
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 )
737 dest = self._tags_v1 if legacy_api else self._tags_v2
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]
765 else:
766 # Spec'd length > 1 or undefined
767 # Unspec'd, and length > 1
768 dest[tag] = values
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)
775 def __iter__(self) -> Iterator[int]:
776 return iter(set(self._tagdata) | set(self._tags_v2))
778 def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]:
779 return struct.unpack(self._endian + fmt, data)
781 def _pack(self, fmt: str, *values: Any) -> bytes:
782 return struct.pack(self._endian + fmt, *values)
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 )
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
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
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")
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"
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)
834 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
835 return (a, b) if legacy_api else IFDRational(a, b)
837 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
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 )
845 @_register_loader(7, 1)
846 def load_undefined(self, data: bytes, legacy_api: bool = True) -> bytes:
847 return data
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
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)
863 def combine(a: int, b: int) -> tuple[int, int] | IFDRational:
864 return (a, b) if legacy_api else IFDRational(a, b)
866 return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
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 )
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
885 def load(self, fp: IO[bytes]) -> None:
886 self.reset()
887 self._offset = fp.tell()
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 )
902 tagname = TiffTags.lookup(tag, self.group).name
903 typname = TYPES.get(typ, "unknown")
904 msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"
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]
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
931 if not data:
932 logger.debug(msg)
933 continue
935 self._tagdata[tag] = data
936 self.tagtype[tag] = typ
938 msg += " - value: "
939 msg += f"<table: {size} bytes>" if size > 32 else repr(data)
941 logger.debug(msg)
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
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)
958 return ifh
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))
964 entries: list[tuple[int, int, int, bytes, bytes]] = []
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
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)
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)
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
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
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 )
1029 # -- overwrite here for multi-page --
1030 result += self._pack(fmt, 0) # end of entries
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"
1038 return result
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())
1044 offset = fp.tell()
1045 result = self.tobytes(offset)
1046 fp.write(result)
1047 return offset + len(result)
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
1059# Legacy ImageFileDirectory support.
1060class ImageFileDirectory_v1(ImageFileDirectory_v2):
1061 """This class represents the **legacy** interface to a TIFF tag directory.
1063 Exposes a dictionary interface of the tags in the directory::
1065 ifd = ImageFileDirectory_v1()
1066 ifd[key] = 'Some Data'
1067 ifd.tagtype[key] = TiffTags.ASCII
1068 print(ifd[key])
1069 ('Some Data',)
1071 Also contains a dictionary of tag types as read from the tiff image file,
1072 :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
1074 Values are returned as a tuple.
1076 .. deprecated:: 3.0.0
1077 """
1079 def __init__(self, *args: Any, **kwargs: Any) -> None:
1080 super().__init__(*args, **kwargs)
1081 self._legacy_api = True
1083 tags = property(lambda self: self._tags_v1)
1084 tagdata = property(lambda self: self._tagdata)
1086 # defined in ImageFileDirectory_v2
1087 tagtype: dict[int, int]
1088 """Dictionary of tag types"""
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.
1098 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
1100 """
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
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.
1115 :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
1117 """
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
1125 def __contains__(self, tag: object) -> bool:
1126 return tag in self._tags_v1 or tag in self._tagdata
1128 def __len__(self) -> int:
1129 return len(set(self._tagdata) | set(self._tags_v1))
1131 def __iter__(self) -> Iterator[int]:
1132 return iter(set(self._tagdata) | set(self._tags_v1))
1134 def __setitem__(self, tag: int, value: Any) -> None:
1135 for legacy_api in (False, True):
1136 self._setitem(tag, value, legacy_api)
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
1151# undone -- switch this pointer
1152ImageFileDirectory = ImageFileDirectory_v1
1155##
1156# Image plugin for TIFF files.
1159class TiffImageFile(ImageFile.ImageFile):
1160 format = "TIFF"
1161 format_description = "Adobe TIFF"
1162 _close_exclusive_fp_after_loading = False
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) """
1172 self.tag: ImageFileDirectory_v1
1173 """ Legacy tag entries """
1175 super().__init__(fp, filename)
1177 def _open(self) -> None:
1178 """Open the first image in a TIFF file"""
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)
1186 self.tag_v2 = ImageFileDirectory_v2(ifh)
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
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)
1199 # and load the first frame
1200 self._seek(0)
1202 @property
1203 def n_frames(self) -> int:
1204 current_n_frames = self._n_frames
1205 if current_n_frames is None:
1206 current = self.tell()
1207 self._seek(len(self._frame_pos))
1208 while self._n_frames is None:
1209 self._seek(self.tell() + 1)
1210 self.seek(current)
1211 assert self._n_frames is not None
1212 return self._n_frames
1214 def seek(self, frame: int) -> None:
1215 """Select a given frame as current image"""
1216 if not self._seek_check(frame):
1217 return
1218 self._seek(frame)
1219 if self._im is not None and (
1220 self.im.size != self._tile_size
1221 or self.im.mode != self.mode
1222 or self.readonly
1223 ):
1224 self._im = None
1226 def _seek(self, frame: int) -> None:
1227 if isinstance(self._fp, DeferredError):
1228 raise self._fp.ex
1229 self.fp = self._fp
1231 while len(self._frame_pos) <= frame:
1232 if not self.__next:
1233 msg = "no more images in TIFF file"
1234 raise EOFError(msg)
1235 logger.debug(
1236 "Seeking to frame %s, on frame %s, __next %s, location: %s",
1237 frame,
1238 self.__frame,
1239 self.__next,
1240 self.fp.tell(),
1241 )
1242 if self.__next >= 2**63:
1243 msg = "Unable to seek to frame"
1244 raise ValueError(msg)
1245 self.fp.seek(self.__next)
1246 self._frame_pos.append(self.__next)
1247 logger.debug("Loading tags, location: %s", self.fp.tell())
1248 self.tag_v2.load(self.fp)
1249 if self.tag_v2.next in self._frame_pos:
1250 # This IFD has already been processed
1251 # Declare this to be the end of the image
1252 self.__next = 0
1253 else:
1254 self.__next = self.tag_v2.next
1255 if self.__next == 0:
1256 self._n_frames = frame + 1
1257 if len(self._frame_pos) == 1:
1258 self.is_animated = self.__next != 0
1259 self.__frame += 1
1260 self.fp.seek(self._frame_pos[frame])
1261 self.tag_v2.load(self.fp)
1262 if XMP in self.tag_v2:
1263 xmp = self.tag_v2[XMP]
1264 if isinstance(xmp, tuple) and len(xmp) == 1:
1265 xmp = xmp[0]
1266 self.info["xmp"] = xmp
1267 elif "xmp" in self.info:
1268 del self.info["xmp"]
1269 self._reload_exif()
1270 # fill the legacy tag/ifd entries
1271 self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
1272 self.__frame = frame
1273 self._setup()
1275 def tell(self) -> int:
1276 """Return the current frame number"""
1277 return self.__frame
1279 def get_photoshop_blocks(self) -> dict[int, dict[str, bytes]]:
1280 """
1281 Returns a dictionary of Photoshop "Image Resource Blocks".
1282 The keys are the image resource ID. For more information, see
1283 https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727
1285 :returns: Photoshop "Image Resource Blocks" in a dictionary.
1286 """
1287 blocks = {}
1288 val = self.tag_v2.get(ExifTags.Base.ImageResources)
1289 if val:
1290 while val.startswith(b"8BIM"):
1291 id = i16(val[4:6])
1292 n = math.ceil((val[6] + 1) / 2) * 2
1293 size = i32(val[6 + n : 10 + n])
1294 data = val[10 + n : 10 + n + size]
1295 blocks[id] = {"data": data}
1297 val = val[math.ceil((10 + n + size) / 2) * 2 :]
1298 return blocks
1300 def load(self) -> Image.core.PixelAccess | None:
1301 if self.tile and self.use_load_libtiff:
1302 return self._load_libtiff()
1303 return super().load()
1305 def load_prepare(self) -> None:
1306 if self._im is None:
1307 Image._decompression_bomb_check(self._tile_size)
1308 self.im = Image.core.new(self.mode, self._tile_size)
1309 ImageFile.ImageFile.load_prepare(self)
1311 def load_end(self) -> None:
1312 # allow closing if we're on the first frame, there's no next
1313 # This is the ImageFile.load path only, libtiff specific below.
1314 if not self.is_animated:
1315 self._close_exclusive_fp_after_loading = True
1317 # load IFD data from fp before it is closed
1318 exif = self.getexif()
1319 for key in TiffTags.TAGS_V2_GROUPS:
1320 if key not in exif:
1321 continue
1322 exif.get_ifd(key)
1324 ImageOps.exif_transpose(self, in_place=True)
1325 if ExifTags.Base.Orientation in self.tag_v2:
1326 del self.tag_v2[ExifTags.Base.Orientation]
1328 def _load_libtiff(self) -> Image.core.PixelAccess | None:
1329 """Overload method triggered when we detect a compressed tiff
1330 Calls out to libtiff"""
1332 Image.Image.load(self)
1334 self.load_prepare()
1336 if not len(self.tile) == 1:
1337 msg = "Not exactly one tile"
1338 raise OSError(msg)
1340 # (self._compression, (extents tuple),
1341 # 0, (rawmode, self._compression, fp))
1342 extents = self.tile[0][1]
1343 args = self.tile[0][3]
1345 # To be nice on memory footprint, if there's a
1346 # file descriptor, use that instead of reading
1347 # into a string in python.
1348 assert self.fp is not None
1349 try:
1350 fp = hasattr(self.fp, "fileno") and self.fp.fileno()
1351 # flush the file descriptor, prevents error on pypy 2.4+
1352 # should also eliminate the need for fp.tell
1353 # in _seek
1354 if hasattr(self.fp, "flush"):
1355 self.fp.flush()
1356 except OSError:
1357 # io.BytesIO have a fileno, but returns an OSError if
1358 # it doesn't use a file descriptor.
1359 fp = False
1361 if fp:
1362 assert isinstance(args, tuple)
1363 args_list = list(args)
1364 args_list[2] = fp
1365 args = tuple(args_list)
1367 decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig)
1368 try:
1369 decoder.setimage(self.im, extents)
1370 except ValueError as e:
1371 msg = "Couldn't set the image"
1372 raise OSError(msg) from e
1374 close_self_fp = self._exclusive_fp and not self.is_animated
1375 if hasattr(self.fp, "getvalue"):
1376 # We've got a stringio like thing passed in. Yay for all in memory.
1377 # The decoder needs the entire file in one shot, so there's not
1378 # a lot we can do here other than give it the entire file.
1379 # unless we could do something like get the address of the
1380 # underlying string for stringio.
1381 #
1382 # Rearranging for supporting byteio items, since they have a fileno
1383 # that returns an OSError if there's no underlying fp. Easier to
1384 # deal with here by reordering.
1385 logger.debug("have getvalue. just sending in a string from getvalue")
1386 n, err = decoder.decode(self.fp.getvalue())
1387 elif fp:
1388 # we've got a actual file on disk, pass in the fp.
1389 logger.debug("have fileno, calling fileno version of the decoder.")
1390 if not close_self_fp:
1391 self.fp.seek(0)
1392 # Save and restore the file position, because libtiff will move it
1393 # outside of the Python runtime, and that will confuse
1394 # io.BufferedReader and possible others.
1395 # NOTE: This must use os.lseek(), and not fp.tell()/fp.seek(),
1396 # because the buffer read head already may not equal the actual
1397 # file position, and fp.seek() may just adjust it's internal
1398 # pointer and not actually seek the OS file handle.
1399 pos = os.lseek(fp, 0, os.SEEK_CUR)
1400 # 4 bytes, otherwise the trace might error out
1401 n, err = decoder.decode(b"fpfp")
1402 os.lseek(fp, pos, os.SEEK_SET)
1403 else:
1404 # we have something else.
1405 logger.debug("don't have fileno or getvalue. just reading")
1406 self.fp.seek(0)
1407 # UNDONE -- so much for that buffer size thing.
1408 n, err = decoder.decode(self.fp.read())
1410 self.tile = []
1411 self.readonly = 0
1413 self.load_end()
1415 if close_self_fp:
1416 self.fp.close()
1417 self.fp = None # might be shared
1419 if err < 0:
1420 msg = f"decoder error {err}"
1421 raise OSError(msg)
1423 return Image.Image.load(self)
1425 def _setup(self) -> None:
1426 """Setup this image object based on current tags"""
1428 if 0xBC01 in self.tag_v2:
1429 msg = "Windows Media Photo files not yet supported"
1430 raise OSError(msg)
1432 # extract relevant tags
1433 self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
1434 self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)
1436 # photometric is a required tag, but not everyone is reading
1437 # the specification
1438 photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
1440 # old style jpeg compression images most certainly are YCbCr
1441 if self._compression == "tiff_jpeg":
1442 photo = 6
1444 fillorder = self.tag_v2.get(FILLORDER, 1)
1446 logger.debug("*** Summary ***")
1447 logger.debug("- compression: %s", self._compression)
1448 logger.debug("- photometric_interpretation: %s", photo)
1449 logger.debug("- planar_configuration: %s", self._planar_configuration)
1450 logger.debug("- fill_order: %s", fillorder)
1451 logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
1453 # size
1454 try:
1455 xsize = self.tag_v2[IMAGEWIDTH]
1456 ysize = self.tag_v2[IMAGELENGTH]
1457 except KeyError as e:
1458 msg = "Missing dimensions"
1459 raise TypeError(msg) from e
1460 if not isinstance(xsize, int) or not isinstance(ysize, int):
1461 msg = "Invalid dimensions"
1462 raise ValueError(msg)
1463 self._tile_size = xsize, ysize
1464 orientation = self.tag_v2.get(ExifTags.Base.Orientation)
1465 if orientation in (5, 6, 7, 8):
1466 self._size = ysize, xsize
1467 else:
1468 self._size = xsize, ysize
1470 logger.debug("- size: %s", self.size)
1472 sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
1473 if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
1474 # SAMPLEFORMAT is properly per band, so an RGB image will
1475 # be (1,1,1). But, we don't support per band pixel types,
1476 # and anything more than one band is a uint8. So, just
1477 # take the first element. Revisit this if adding support
1478 # for more exotic images.
1479 sample_format = (1,)
1481 bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
1482 extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
1483 if photo in (2, 6, 8): # RGB, YCbCr, LAB
1484 bps_count = 3
1485 elif photo == 5: # CMYK
1486 bps_count = 4
1487 else:
1488 bps_count = 1
1489 bps_count += len(extra_tuple)
1490 bps_actual_count = len(bps_tuple)
1491 samples_per_pixel = self.tag_v2.get(
1492 SAMPLESPERPIXEL,
1493 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
1494 )
1496 if samples_per_pixel > MAX_SAMPLESPERPIXEL:
1497 # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
1498 logger.error(
1499 "More samples per pixel than can be decoded: %s", samples_per_pixel
1500 )
1501 msg = "Invalid value for samples per pixel"
1502 raise SyntaxError(msg)
1504 if samples_per_pixel < bps_actual_count:
1505 # If a file has more values in bps_tuple than expected,
1506 # remove the excess.
1507 bps_tuple = bps_tuple[:samples_per_pixel]
1508 elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
1509 # If a file has only one value in bps_tuple, when it should have more,
1510 # presume it is the same number of bits for all of the samples.
1511 bps_tuple = bps_tuple * samples_per_pixel
1513 if len(bps_tuple) != samples_per_pixel:
1514 msg = "unknown data organization"
1515 raise SyntaxError(msg)
1517 # mode: check photometric interpretation and bits per pixel
1518 key = (
1519 self.tag_v2.prefix,
1520 photo,
1521 sample_format,
1522 fillorder,
1523 bps_tuple,
1524 extra_tuple,
1525 )
1526 logger.debug("format key: %s", key)
1527 try:
1528 self._mode, rawmode = OPEN_INFO[key]
1529 except KeyError as e:
1530 logger.debug("- unsupported format")
1531 msg = "unknown pixel mode"
1532 raise SyntaxError(msg) from e
1534 logger.debug("- raw mode: %s", rawmode)
1535 logger.debug("- pil mode: %s", self.mode)
1537 self.info["compression"] = self._compression
1539 xres = self.tag_v2.get(X_RESOLUTION, 1)
1540 yres = self.tag_v2.get(Y_RESOLUTION, 1)
1542 if xres and yres:
1543 resunit = self.tag_v2.get(RESOLUTION_UNIT)
1544 if resunit == 2: # dots per inch
1545 self.info["dpi"] = (xres, yres)
1546 elif resunit == 3: # dots per centimeter. convert to dpi
1547 self.info["dpi"] = (xres * 2.54, yres * 2.54)
1548 elif resunit is None: # used to default to 1, but now 2)
1549 self.info["dpi"] = (xres, yres)
1550 # For backward compatibility,
1551 # we also preserve the old behavior
1552 self.info["resolution"] = xres, yres
1553 else: # No absolute unit of measurement
1554 self.info["resolution"] = xres, yres
1556 # build tile descriptors
1557 x = y = layer = 0
1558 self.tile = []
1559 self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
1560 if self.use_load_libtiff:
1561 # Decoder expects entire file as one tile.
1562 # There's a buffer size limit in load (64k)
1563 # so large g4 images will fail if we use that
1564 # function.
1565 #
1566 # Setup the one tile for the whole image, then
1567 # use the _load_libtiff function.
1569 # libtiff handles the fillmode for us, so 1;IR should
1570 # actually be 1;I. Including the R double reverses the
1571 # bits, so stripes of the image are reversed. See
1572 # https://github.com/python-pillow/Pillow/issues/279
1573 if fillorder == 2:
1574 # Replace fillorder with fillorder=1
1575 key = key[:3] + (1,) + key[4:]
1576 logger.debug("format key: %s", key)
1577 # this should always work, since all the
1578 # fillorder==2 modes have a corresponding
1579 # fillorder=1 mode
1580 self._mode, rawmode = OPEN_INFO[key]
1581 # YCbCr images with new jpeg compression with pixels in one plane
1582 # unpacked straight into RGB values
1583 if (
1584 photo == 6
1585 and self._compression == "jpeg"
1586 and self._planar_configuration == 1
1587 ):
1588 rawmode = "RGB"
1589 # libtiff always returns the bytes in native order.
1590 # we're expecting image byte order. So, if the rawmode
1591 # contains I;16, we need to convert from native to image
1592 # byte order.
1593 elif rawmode == "I;16":
1594 rawmode = "I;16N"
1595 elif rawmode.endswith((";16B", ";16L")):
1596 rawmode = rawmode[:-1] + "N"
1598 # Offset in the tile tuple is 0, we go from 0,0 to
1599 # w,h, and we only do this once -- eds
1600 a = (rawmode, self._compression, False, self.tag_v2.offset)
1601 self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a))
1603 elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
1604 # striped image
1605 if STRIPOFFSETS in self.tag_v2:
1606 offsets = self.tag_v2[STRIPOFFSETS]
1607 h = self.tag_v2.get(ROWSPERSTRIP, ysize)
1608 w = xsize
1609 else:
1610 # tiled image
1611 offsets = self.tag_v2[TILEOFFSETS]
1612 tilewidth = self.tag_v2.get(TILEWIDTH)
1613 h = self.tag_v2.get(TILELENGTH)
1614 if not isinstance(tilewidth, int) or not isinstance(h, int):
1615 msg = "Invalid tile dimensions"
1616 raise ValueError(msg)
1617 w = tilewidth
1619 if w == xsize and h == ysize and self._planar_configuration != 2:
1620 # Every tile covers the image. Only use the last offset
1621 offsets = offsets[-1:]
1623 for offset in offsets:
1624 if x + w > xsize:
1625 stride = w * sum(bps_tuple) / 8 # bytes per line
1626 else:
1627 stride = 0
1629 tile_rawmode = rawmode
1630 if self._planar_configuration == 2:
1631 # each band on it's own layer
1632 tile_rawmode = rawmode[layer]
1633 # adjust stride width accordingly
1634 stride /= bps_count
1636 args = (tile_rawmode, int(stride), 1)
1637 self.tile.append(
1638 ImageFile._Tile(
1639 self._compression,
1640 (x, y, min(x + w, xsize), min(y + h, ysize)),
1641 offset,
1642 args,
1643 )
1644 )
1645 x += w
1646 if x >= xsize:
1647 x, y = 0, y + h
1648 if y >= ysize:
1649 y = 0
1650 layer += 1
1651 else:
1652 logger.debug("- unsupported data organization")
1653 msg = "unknown data organization"
1654 raise SyntaxError(msg)
1656 # Fix up info.
1657 if ICCPROFILE in self.tag_v2:
1658 self.info["icc_profile"] = self.tag_v2[ICCPROFILE]
1660 # fixup palette descriptor
1662 if self.mode in ["P", "PA"]:
1663 palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
1664 self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
1667#
1668# --------------------------------------------------------------------
1669# Write TIFF files
1671# little endian is default except for image modes with
1672# explicit big endian byte-order
1674SAVE_INFO = {
1675 # mode => rawmode, byteorder, photometrics,
1676 # sampleformat, bitspersample, extra
1677 "1": ("1", II, 1, 1, (1,), None),
1678 "L": ("L", II, 1, 1, (8,), None),
1679 "LA": ("LA", II, 1, 1, (8, 8), 2),
1680 "P": ("P", II, 3, 1, (8,), None),
1681 "PA": ("PA", II, 3, 1, (8, 8), 2),
1682 "I": ("I;32S", II, 1, 2, (32,), None),
1683 "I;16": ("I;16", II, 1, 1, (16,), None),
1684 "I;16L": ("I;16L", II, 1, 1, (16,), None),
1685 "F": ("F;32F", II, 1, 3, (32,), None),
1686 "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
1687 "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
1688 "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
1689 "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
1690 "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
1691 "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
1692 "I;16B": ("I;16B", MM, 1, 1, (16,), None),
1693}
1696def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
1697 try:
1698 rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
1699 except KeyError as e:
1700 msg = f"cannot write mode {im.mode} as TIFF"
1701 raise OSError(msg) from e
1703 encoderinfo = im.encoderinfo
1704 encoderconfig = im.encoderconfig
1706 ifd = ImageFileDirectory_v2(prefix=prefix)
1707 if encoderinfo.get("big_tiff"):
1708 ifd._bigtiff = True
1710 try:
1711 compression = encoderinfo["compression"]
1712 except KeyError:
1713 compression = im.info.get("compression")
1714 if isinstance(compression, int):
1715 # compression value may be from BMP. Ignore it
1716 compression = None
1717 if compression is None:
1718 compression = "raw"
1719 elif compression == "tiff_jpeg":
1720 # OJPEG is obsolete, so use new-style JPEG compression instead
1721 compression = "jpeg"
1722 elif compression == "tiff_deflate":
1723 compression = "tiff_adobe_deflate"
1725 libtiff = WRITE_LIBTIFF or compression != "raw"
1727 # required for color libtiff images
1728 ifd[PLANAR_CONFIGURATION] = 1
1730 ifd[IMAGEWIDTH] = im.size[0]
1731 ifd[IMAGELENGTH] = im.size[1]
1733 # write any arbitrary tags passed in as an ImageFileDirectory
1734 if "tiffinfo" in encoderinfo:
1735 info = encoderinfo["tiffinfo"]
1736 elif "exif" in encoderinfo:
1737 info = encoderinfo["exif"]
1738 if isinstance(info, bytes):
1739 exif = Image.Exif()
1740 exif.load(info)
1741 info = exif
1742 else:
1743 info = {}
1744 logger.debug("Tiffinfo Keys: %s", list(info))
1745 if isinstance(info, ImageFileDirectory_v1):
1746 info = info.to_v2()
1747 for key in info:
1748 if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS:
1749 ifd[key] = info.get_ifd(key)
1750 else:
1751 ifd[key] = info.get(key)
1752 try:
1753 ifd.tagtype[key] = info.tagtype[key]
1754 except Exception:
1755 pass # might not be an IFD. Might not have populated type
1757 legacy_ifd = {}
1758 if hasattr(im, "tag"):
1759 legacy_ifd = im.tag.to_v2()
1761 supplied_tags = {**legacy_ifd, **getattr(im, "tag_v2", {})}
1762 for tag in (
1763 # IFD offset that may not be correct in the saved image
1764 EXIFIFD,
1765 # Determined by the image format and should not be copied from legacy_ifd.
1766 SAMPLEFORMAT,
1767 ):
1768 if tag in supplied_tags:
1769 del supplied_tags[tag]
1771 # additions written by Greg Couch, gregc@cgl.ucsf.edu
1772 # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
1773 if hasattr(im, "tag_v2"):
1774 # preserve tags from original TIFF image file
1775 for key in (
1776 RESOLUTION_UNIT,
1777 X_RESOLUTION,
1778 Y_RESOLUTION,
1779 IPTC_NAA_CHUNK,
1780 PHOTOSHOP_CHUNK,
1781 XMP,
1782 ):
1783 if key in im.tag_v2:
1784 if key == IPTC_NAA_CHUNK and im.tag_v2.tagtype[key] not in (
1785 TiffTags.BYTE,
1786 TiffTags.UNDEFINED,
1787 ):
1788 del supplied_tags[key]
1789 else:
1790 ifd[key] = im.tag_v2[key]
1791 ifd.tagtype[key] = im.tag_v2.tagtype[key]
1793 # preserve ICC profile (should also work when saving other formats
1794 # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
1795 icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
1796 if icc:
1797 ifd[ICCPROFILE] = icc
1799 for key, name in [
1800 (IMAGEDESCRIPTION, "description"),
1801 (X_RESOLUTION, "resolution"),
1802 (Y_RESOLUTION, "resolution"),
1803 (X_RESOLUTION, "x_resolution"),
1804 (Y_RESOLUTION, "y_resolution"),
1805 (RESOLUTION_UNIT, "resolution_unit"),
1806 (SOFTWARE, "software"),
1807 (DATE_TIME, "date_time"),
1808 (ARTIST, "artist"),
1809 (COPYRIGHT, "copyright"),
1810 ]:
1811 if name in encoderinfo:
1812 ifd[key] = encoderinfo[name]
1814 dpi = encoderinfo.get("dpi")
1815 if dpi:
1816 ifd[RESOLUTION_UNIT] = 2
1817 ifd[X_RESOLUTION] = dpi[0]
1818 ifd[Y_RESOLUTION] = dpi[1]
1820 if bits != (1,):
1821 ifd[BITSPERSAMPLE] = bits
1822 if len(bits) != 1:
1823 ifd[SAMPLESPERPIXEL] = len(bits)
1824 if extra is not None:
1825 ifd[EXTRASAMPLES] = extra
1826 if format != 1:
1827 ifd[SAMPLEFORMAT] = format
1829 if PHOTOMETRIC_INTERPRETATION not in ifd:
1830 ifd[PHOTOMETRIC_INTERPRETATION] = photo
1831 elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
1832 if im.mode == "1":
1833 inverted_im = im.copy()
1834 px = inverted_im.load()
1835 if px is not None:
1836 for y in range(inverted_im.height):
1837 for x in range(inverted_im.width):
1838 px[x, y] = 0 if px[x, y] == 255 else 255
1839 im = inverted_im
1840 else:
1841 im = ImageOps.invert(im)
1843 if im.mode in ["P", "PA"]:
1844 lut = im.im.getpalette("RGB", "RGB;L")
1845 colormap = []
1846 colors = len(lut) // 3
1847 for i in range(3):
1848 colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]
1849 colormap += [0] * (256 - colors)
1850 ifd[COLORMAP] = colormap
1851 # data orientation
1852 w, h = ifd[IMAGEWIDTH], ifd[IMAGELENGTH]
1853 stride = len(bits) * ((w * bits[0] + 7) // 8)
1854 if ROWSPERSTRIP not in ifd:
1855 # aim for given strip size (64 KB by default) when using libtiff writer
1856 if libtiff:
1857 im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
1858 rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, h)
1859 # JPEG encoder expects multiple of 8 rows
1860 if compression == "jpeg":
1861 rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, h)
1862 else:
1863 rows_per_strip = h
1864 if rows_per_strip == 0:
1865 rows_per_strip = 1
1866 ifd[ROWSPERSTRIP] = rows_per_strip
1867 strip_byte_counts = 1 if stride == 0 else stride * ifd[ROWSPERSTRIP]
1868 strips_per_image = (h + ifd[ROWSPERSTRIP] - 1) // ifd[ROWSPERSTRIP]
1869 if strip_byte_counts >= 2**16:
1870 ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
1871 ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
1872 stride * h - strip_byte_counts * (strips_per_image - 1),
1873 )
1874 ifd[STRIPOFFSETS] = tuple(
1875 range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
1876 ) # this is adjusted by IFD writer
1877 # no compression by default:
1878 ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
1880 if im.mode == "YCbCr":
1881 for tag, default_value in {
1882 YCBCRSUBSAMPLING: (1, 1),
1883 REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
1884 }.items():
1885 ifd.setdefault(tag, default_value)
1887 blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
1888 if libtiff:
1889 if "quality" in encoderinfo:
1890 quality = encoderinfo["quality"]
1891 if not isinstance(quality, int) or quality < 0 or quality > 100:
1892 msg = "Invalid quality setting"
1893 raise ValueError(msg)
1894 if compression != "jpeg":
1895 msg = "quality setting only supported for 'jpeg' compression"
1896 raise ValueError(msg)
1897 ifd[JPEGQUALITY] = quality
1899 logger.debug("Saving using libtiff encoder")
1900 logger.debug("Items: %s", sorted(ifd.items()))
1901 _fp = 0
1902 if hasattr(fp, "fileno"):
1903 try:
1904 fp.seek(0)
1905 _fp = fp.fileno()
1906 except io.UnsupportedOperation:
1907 pass
1909 # optional types for non core tags
1910 types = {}
1911 # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
1912 # based on the data in the strip.
1913 # OSUBFILETYPE is deprecated.
1914 # The other tags expect arrays with a certain length (fixed or depending on
1915 # BITSPERSAMPLE, etc), passing arrays with a different length will result in
1916 # segfaults. Block these tags until we add extra validation.
1917 # SUBIFD may also cause a segfault.
1918 blocklist += [
1919 OSUBFILETYPE,
1920 REFERENCEBLACKWHITE,
1921 STRIPBYTECOUNTS,
1922 STRIPOFFSETS,
1923 TRANSFERFUNCTION,
1924 SUBIFD,
1925 ]
1927 # bits per sample is a single short in the tiff directory, not a list.
1928 atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
1929 # Merge the ones that we have with (optional) more bits from
1930 # the original file, e.g x,y resolution so that we can
1931 # save(load('')) == original file.
1932 for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
1933 # Libtiff can only process certain core items without adding
1934 # them to the custom dictionary.
1935 # Custom items are supported for int, float, unicode, string and byte
1936 # values. Other types and tuples require a tagtype.
1937 if tag not in TiffTags.LIBTIFF_CORE:
1938 if tag in TiffTags.TAGS_V2_GROUPS:
1939 types[tag] = TiffTags.LONG8
1940 elif tag in ifd.tagtype:
1941 types[tag] = ifd.tagtype[tag]
1942 elif isinstance(value, (int, float, str, bytes)) or (
1943 isinstance(value, tuple)
1944 and all(isinstance(v, (int, float, IFDRational)) for v in value)
1945 ):
1946 type = TiffTags.lookup(tag).type
1947 if type:
1948 types[tag] = type
1949 if tag not in atts and tag not in blocklist:
1950 if isinstance(value, str):
1951 atts[tag] = value.encode("ascii", "replace") + b"\0"
1952 elif isinstance(value, IFDRational):
1953 atts[tag] = float(value)
1954 else:
1955 atts[tag] = value
1957 if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
1958 atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]
1960 logger.debug("Converted items: %s", sorted(atts.items()))
1962 # libtiff always expects the bytes in native order.
1963 # we're storing image byte order. So, if the rawmode
1964 # contains I;16, we need to convert from native to image
1965 # byte order.
1966 if im.mode in ("I;16", "I;16B", "I;16L"):
1967 rawmode = "I;16N"
1969 # Pass tags as sorted list so that the tags are set in a fixed order.
1970 # This is required by libtiff for some tags. For example, the JPEGQUALITY
1971 # pseudo tag requires that the COMPRESS tag was already set.
1972 tags = list(atts.items())
1973 tags.sort()
1974 a = (rawmode, compression, _fp, filename, tags, types)
1975 encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
1976 encoder.setimage(im.im, (0, 0) + im.size)
1977 while True:
1978 errcode, data = encoder.encode(ImageFile.MAXBLOCK)[1:]
1979 if not _fp:
1980 fp.write(data)
1981 if errcode:
1982 break
1983 if errcode < 0:
1984 msg = f"encoder error {errcode} when writing image file"
1985 raise OSError(msg)
1987 else:
1988 for tag in blocklist:
1989 del ifd[tag]
1990 offset = ifd.save(fp)
1992 ImageFile._save(
1993 im,
1994 fp,
1995 [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
1996 )
1998 # -- helper for multi-page save --
1999 if "_debug_multipage" in encoderinfo:
2000 # just to access o32 and o16 (using correct byte order)
2001 setattr(im, "_debug_multipage", ifd)
2004class AppendingTiffWriter(io.BytesIO):
2005 fieldSizes = [
2006 0, # None
2007 1, # byte
2008 1, # ascii
2009 2, # short
2010 4, # long
2011 8, # rational
2012 1, # sbyte
2013 1, # undefined
2014 2, # sshort
2015 4, # slong
2016 8, # srational
2017 4, # float
2018 8, # double
2019 4, # ifd
2020 2, # unicode
2021 4, # complex
2022 8, # long8
2023 ]
2025 Tags = {
2026 273, # StripOffsets
2027 288, # FreeOffsets
2028 324, # TileOffsets
2029 519, # JPEGQTables
2030 520, # JPEGDCTables
2031 521, # JPEGACTables
2032 }
2034 def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None:
2035 self.f: IO[bytes]
2036 if is_path(fn):
2037 self.name = fn
2038 self.close_fp = True
2039 try:
2040 self.f = open(fn, "w+b" if new else "r+b")
2041 except OSError:
2042 self.f = open(fn, "w+b")
2043 else:
2044 self.f = cast(IO[bytes], fn)
2045 self.close_fp = False
2046 self.beginning = self.f.tell()
2047 self.setup()
2049 def setup(self) -> None:
2050 # Reset everything.
2051 self.f.seek(self.beginning, os.SEEK_SET)
2053 self.whereToWriteNewIFDOffset: int | None = None
2054 self.offsetOfNewPage = 0
2056 self.IIMM = iimm = self.f.read(4)
2057 self._bigtiff = b"\x2b" in iimm
2058 if not iimm:
2059 # empty file - first page
2060 self.isFirst = True
2061 return
2063 self.isFirst = False
2064 if iimm not in PREFIXES:
2065 msg = "Invalid TIFF file header"
2066 raise RuntimeError(msg)
2068 self.setEndian("<" if iimm.startswith(II) else ">")
2070 if self._bigtiff:
2071 self.f.seek(4, os.SEEK_CUR)
2072 self.skipIFDs()
2073 self.goToEnd()
2075 def finalize(self) -> None:
2076 if self.isFirst:
2077 return
2079 # fix offsets
2080 self.f.seek(self.offsetOfNewPage)
2082 iimm = self.f.read(4)
2083 if not iimm:
2084 # Make it easy to finish a frame without committing to a new one.
2085 return
2087 if iimm != self.IIMM:
2088 msg = "IIMM of new page doesn't match IIMM of first page"
2089 raise RuntimeError(msg)
2091 if self._bigtiff:
2092 self.f.seek(4, os.SEEK_CUR)
2093 ifd_offset = self._read(8 if self._bigtiff else 4)
2094 ifd_offset += self.offsetOfNewPage
2095 assert self.whereToWriteNewIFDOffset is not None
2096 self.f.seek(self.whereToWriteNewIFDOffset)
2097 self._write(ifd_offset, 8 if self._bigtiff else 4)
2098 self.f.seek(ifd_offset)
2099 self.fixIFD()
2101 def newFrame(self) -> None:
2102 # Call this to finish a frame.
2103 self.finalize()
2104 self.setup()
2106 def __enter__(self) -> AppendingTiffWriter:
2107 return self
2109 def __exit__(self, *args: object) -> None:
2110 if self.close_fp:
2111 self.close()
2113 def tell(self) -> int:
2114 return self.f.tell() - self.offsetOfNewPage
2116 def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
2117 """
2118 :param offset: Distance to seek.
2119 :param whence: Whether the distance is relative to the start,
2120 end or current position.
2121 :returns: The resulting position, relative to the start.
2122 """
2123 if whence == os.SEEK_SET:
2124 offset += self.offsetOfNewPage
2126 self.f.seek(offset, whence)
2127 return self.tell()
2129 def goToEnd(self) -> None:
2130 self.f.seek(0, os.SEEK_END)
2131 pos = self.f.tell()
2133 # pad to 16 byte boundary
2134 pad_bytes = 16 - pos % 16
2135 if 0 < pad_bytes < 16:
2136 self.f.write(bytes(pad_bytes))
2137 self.offsetOfNewPage = self.f.tell()
2139 def setEndian(self, endian: str) -> None:
2140 self.endian = endian
2141 self.longFmt = f"{self.endian}L"
2142 self.shortFmt = f"{self.endian}H"
2143 self.tagFormat = f"{self.endian}HH" + ("Q" if self._bigtiff else "L")
2145 def skipIFDs(self) -> None:
2146 while True:
2147 ifd_offset = self._read(8 if self._bigtiff else 4)
2148 if ifd_offset == 0:
2149 self.whereToWriteNewIFDOffset = self.f.tell() - (
2150 8 if self._bigtiff else 4
2151 )
2152 break
2154 self.f.seek(ifd_offset)
2155 num_tags = self._read(8 if self._bigtiff else 2)
2156 self.f.seek(num_tags * (20 if self._bigtiff else 12), os.SEEK_CUR)
2158 def write(self, data: Buffer, /) -> int:
2159 return self.f.write(data)
2161 def _fmt(self, field_size: int) -> str:
2162 try:
2163 return {2: "H", 4: "L", 8: "Q"}[field_size]
2164 except KeyError:
2165 msg = "offset is not supported"
2166 raise RuntimeError(msg)
2168 def _read(self, field_size: int) -> int:
2169 (value,) = struct.unpack(
2170 self.endian + self._fmt(field_size), self.f.read(field_size)
2171 )
2172 return value
2174 def readShort(self) -> int:
2175 return self._read(2)
2177 def readLong(self) -> int:
2178 return self._read(4)
2180 @staticmethod
2181 def _verify_bytes_written(bytes_written: int | None, expected: int) -> None:
2182 if bytes_written is not None and bytes_written != expected:
2183 msg = f"wrote only {bytes_written} bytes but wanted {expected}"
2184 raise RuntimeError(msg)
2186 def _rewriteLast(
2187 self, value: int, field_size: int, new_field_size: int = 0
2188 ) -> None:
2189 self.f.seek(-field_size, os.SEEK_CUR)
2190 if not new_field_size:
2191 new_field_size = field_size
2192 bytes_written = self.f.write(
2193 struct.pack(self.endian + self._fmt(new_field_size), value)
2194 )
2195 self._verify_bytes_written(bytes_written, new_field_size)
2197 def rewriteLastShortToLong(self, value: int) -> None:
2198 self._rewriteLast(value, 2, 4)
2200 def rewriteLastShort(self, value: int) -> None:
2201 return self._rewriteLast(value, 2)
2203 def rewriteLastLong(self, value: int) -> None:
2204 return self._rewriteLast(value, 4)
2206 def _write(self, value: int, field_size: int) -> None:
2207 bytes_written = self.f.write(
2208 struct.pack(self.endian + self._fmt(field_size), value)
2209 )
2210 self._verify_bytes_written(bytes_written, field_size)
2212 def writeShort(self, value: int) -> None:
2213 self._write(value, 2)
2215 def writeLong(self, value: int) -> None:
2216 self._write(value, 4)
2218 def close(self) -> None:
2219 self.finalize()
2220 if self.close_fp:
2221 self.f.close()
2223 def fixIFD(self) -> None:
2224 num_tags = self._read(8 if self._bigtiff else 2)
2226 for i in range(num_tags):
2227 tag, field_type, count = struct.unpack(
2228 self.tagFormat, self.f.read(12 if self._bigtiff else 8)
2229 )
2231 field_size = self.fieldSizes[field_type]
2232 total_size = field_size * count
2233 fmt_size = 8 if self._bigtiff else 4
2234 is_local = total_size <= fmt_size
2235 if not is_local:
2236 offset = self._read(fmt_size) + self.offsetOfNewPage
2237 self._rewriteLast(offset, fmt_size)
2239 if tag in self.Tags:
2240 cur_pos = self.f.tell()
2242 logger.debug(
2243 "fixIFD: %s (%d) - type: %s (%d) - type size: %d - count: %d",
2244 TiffTags.lookup(tag).name,
2245 tag,
2246 TYPES.get(field_type, "unknown"),
2247 field_type,
2248 field_size,
2249 count,
2250 )
2252 if is_local:
2253 self._fixOffsets(count, field_size)
2254 self.f.seek(cur_pos + fmt_size)
2255 else:
2256 self.f.seek(offset)
2257 self._fixOffsets(count, field_size)
2258 self.f.seek(cur_pos)
2260 elif is_local:
2261 # skip the locally stored value that is not an offset
2262 self.f.seek(fmt_size, os.SEEK_CUR)
2264 def _fixOffsets(self, count: int, field_size: int) -> None:
2265 for i in range(count):
2266 offset = self._read(field_size)
2267 offset += self.offsetOfNewPage
2269 new_field_size = 0
2270 if self._bigtiff and field_size in (2, 4) and offset >= 2**32:
2271 # offset is now too large - we must convert long to long8
2272 new_field_size = 8
2273 elif field_size == 2 and offset >= 2**16:
2274 # offset is now too large - we must convert short to long
2275 new_field_size = 4
2276 if new_field_size:
2277 if count != 1:
2278 msg = "not implemented"
2279 raise RuntimeError(msg) # XXX TODO
2281 # simple case - the offset is just one and therefore it is
2282 # local (not referenced with another offset)
2283 self._rewriteLast(offset, field_size, new_field_size)
2284 # Move back past the new offset, past 'count', and before 'field_type'
2285 rewind = -new_field_size - 4 - 2
2286 self.f.seek(rewind, os.SEEK_CUR)
2287 self.writeShort(new_field_size) # rewrite the type
2288 self.f.seek(2 - rewind, os.SEEK_CUR)
2289 else:
2290 self._rewriteLast(offset, field_size)
2292 def fixOffsets(
2293 self, count: int, isShort: bool = False, isLong: bool = False
2294 ) -> None:
2295 if isShort:
2296 field_size = 2
2297 elif isLong:
2298 field_size = 4
2299 else:
2300 field_size = 0
2301 return self._fixOffsets(count, field_size)
2304def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
2305 append_images = list(im.encoderinfo.get("append_images", []))
2306 if not hasattr(im, "n_frames") and not append_images:
2307 return _save(im, fp, filename)
2309 cur_idx = im.tell()
2310 try:
2311 with AppendingTiffWriter(fp) as tf:
2312 for ims in [im] + append_images:
2313 encoderinfo = ims._attach_default_encoderinfo(im)
2314 if not hasattr(ims, "encoderconfig"):
2315 ims.encoderconfig = ()
2316 nfr = getattr(ims, "n_frames", 1)
2318 for idx in range(nfr):
2319 ims.seek(idx)
2320 ims.load()
2321 _save(ims, tf, filename)
2322 tf.newFrame()
2323 ims.encoderinfo = encoderinfo
2324 finally:
2325 im.seek(cur_idx)
2328#
2329# --------------------------------------------------------------------
2330# Register
2332Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
2333Image.register_save(TiffImageFile.format, _save)
2334Image.register_save_all(TiffImageFile.format, _save_all)
2336Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
2338Image.register_mime(TiffImageFile.format, "image/tiff")