# # The Python Imaging Library. # $Id$ # # TIFF file handling # # TIFF is a flexible, if somewhat aged, image file format originally # defined by Aldus. Although TIFF supports a wide variety of pixel # layouts and compression methods, the name doesn't really stand for # "thousands of incompatible file formats," it just feels that way. # # To read TIFF data from a stream, the stream must be seekable. For # progressive decoding, make sure to use TIFF files where the tag # directory is placed first in the file. # # History: # 1995-09-01 fl Created # 1996-05-04 fl Handle JPEGTABLES tag # 1996-05-18 fl Fixed COLORMAP support # 1997-01-05 fl Fixed PREDICTOR support # 1997-08-27 fl Added support for rational tags (from Perry Stoll) # 1998-01-10 fl Fixed seek/tell (from Jan Blom) # 1998-07-15 fl Use private names for internal variables # 1999-06-13 fl Rewritten for PIL 1.0 (1.0) # 2000-10-11 fl Additional fixes for Python 2.0 (1.1) # 2001-04-17 fl Fixed rewind support (seek to frame 0) (1.2) # 2001-05-12 fl Added write support for more tags (from Greg Couch) (1.3) # 2001-12-18 fl Added workaround for broken Matrox library # 2002-01-18 fl Don't mess up if photometric tag is missing (D. Alan Stewart) # 2003-05-19 fl Check FILLORDER tag # 2003-09-26 fl Added RGBa support # 2004-02-24 fl Added DPI support; fixed rational write support # 2005-02-07 fl Added workaround for broken Corel Draw 10 files # 2006-01-09 fl Added support for float/double tags (from Russell Nelson) # # Copyright (c) 1997-2006 by Secret Labs AB. All rights reserved. # Copyright (c) 1995-1997 by Fredrik Lundh # # See the README file for information on usage and redistribution. # import io import itertools import logging import os import struct import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational from . import Image, ImageFile, ImagePalette, TiffTags from ._binary import o8 from .TiffTags import TYPES logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False IFD_LEGACY_API = True II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) # # -------------------------------------------------------------------- # Read TIFF files # a few tag names, just to make the code below a bit more readable IMAGEWIDTH = 256 IMAGELENGTH = 257 BITSPERSAMPLE = 258 COMPRESSION = 259 PHOTOMETRIC_INTERPRETATION = 262 FILLORDER = 266 IMAGEDESCRIPTION = 270 STRIPOFFSETS = 273 SAMPLESPERPIXEL = 277 ROWSPERSTRIP = 278 STRIPBYTECOUNTS = 279 X_RESOLUTION = 282 Y_RESOLUTION = 283 PLANAR_CONFIGURATION = 284 RESOLUTION_UNIT = 296 TRANSFERFUNCTION = 301 SOFTWARE = 305 DATE_TIME = 306 ARTIST = 315 PREDICTOR = 317 COLORMAP = 320 TILEOFFSETS = 324 SUBIFD = 330 EXTRASAMPLES = 338 SAMPLEFORMAT = 339 JPEGTABLES = 347 REFERENCEBLACKWHITE = 532 COPYRIGHT = 33432 IPTC_NAA_CHUNK = 33723 # newsphoto properties PHOTOSHOP_CHUNK = 34377 # photoshop properties ICCPROFILE = 34675 EXIFIFD = 34665 XMP = 700 JPEGQUALITY = 65537 # pseudo-tag by libtiff # https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java IMAGEJ_META_DATA_BYTE_COUNTS = 50838 IMAGEJ_META_DATA = 50839 COMPRESSION_INFO = { # Compression => pil compression name 1: "raw", 2: "tiff_ccitt", 3: "group3", 4: "group4", 5: "tiff_lzw", 6: "tiff_jpeg", # obsolete 7: "jpeg", 8: "tiff_adobe_deflate", 32771: "tiff_raw_16", # 16-bit padding 32773: "packbits", 32809: "tiff_thunderscan", 32946: "tiff_deflate", 34676: "tiff_sgilog", 34677: "tiff_sgilog24", 34925: "lzma", 50000: "zstd", 50001: "webp", } COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()} OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, # ExtraSamples) => mode, rawmode (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), (II, 1, (1,), 1, (1,), ()): ("1", "1"), (MM, 1, (1,), 1, (1,), ()): ("1", "1"), (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"), (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"), (II, 1, (1,), 1, (2,), ()): ("L", "L;2"), (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"), (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"), (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"), (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"), (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"), (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"), (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"), (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"), (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"), (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"), (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"), (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"), (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"), (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"), (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"), (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"), (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"), (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"), (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"), (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), # JPEG compressed images handled by LibTiff and auto-converted to RGBX # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"), (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order b"II\x2A\x00", # Valid TIFF header with little-endian byte order b"MM\x2A\x00", # Invalid TIFF header, assume big-endian b"II\x00\x2A", # Invalid TIFF header, assume little-endian ] def _accept(prefix): return prefix[:4] in PREFIXES def _limit_rational(val, max_val): inv = abs(val) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d def _limit_signed_rational(val, max_val, min_val): frac = Fraction(val) n_d = frac.numerator, frac.denominator if min(n_d) < min_val: n_d = _limit_rational(val, abs(min_val)) if max(n_d) > max_val: val = Fraction(*n_d) n_d = _limit_rational(val, max_val) return n_d ## # Wrapper for TIFF IFDs. _load_dispatch = {} _write_dispatch = {} class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used """ """ If the denominator is 0, store this as a float('nan'), otherwise store as a fractions.Fraction(). Delegate as appropriate """ __slots__ = ("_numerator", "_denominator", "_val") def __init__(self, value, denominator=1): """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ if isinstance(value, IFDRational): self._numerator = value.numerator self._denominator = value.denominator self._val = value._val return if isinstance(value, Fraction): self._numerator = value.numerator self._denominator = value.denominator else: self._numerator = value self._denominator = denominator if denominator == 0: self._val = float("nan") elif denominator == 1: self._val = Fraction(value) else: self._val = Fraction(value, denominator) @property def numerator(a): return a._numerator @property def denominator(a): return a._denominator def limit_rational(self, max_denominator): """ :param max_denominator: Integer, the maximum denominator value :returns: Tuple of (numerator, denominator) """ if self.denominator == 0: return (self.numerator, self.denominator) f = self._val.limit_denominator(max_denominator) return (f.numerator, f.denominator) def __repr__(self): return str(float(self._val)) def __hash__(self): return self._val.__hash__() def __eq__(self, other): if isinstance(other, IFDRational): other = other._val return self._val == other def _delegate(op): def delegate(self, *args): return getattr(self._val, op)(*args) return delegate """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool', 'ceil', 'floor', 'round'] print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)) """ __add__ = _delegate("__add__") __radd__ = _delegate("__radd__") __sub__ = _delegate("__sub__") __rsub__ = _delegate("__rsub__") __mul__ = _delegate("__mul__") __rmul__ = _delegate("__rmul__") __truediv__ = _delegate("__truediv__") __rtruediv__ = _delegate("__rtruediv__") __floordiv__ = _delegate("__floordiv__") __rfloordiv__ = _delegate("__rfloordiv__") __mod__ = _delegate("__mod__") __rmod__ = _delegate("__rmod__") __pow__ = _delegate("__pow__") __rpow__ = _delegate("__rpow__") __pos__ = _delegate("__pos__") __neg__ = _delegate("__neg__") __abs__ = _delegate("__abs__") __trunc__ = _delegate("__trunc__") __lt__ = _delegate("__lt__") __gt__ = _delegate("__gt__") __le__ = _delegate("__le__") __ge__ = _delegate("__ge__") __bool__ = _delegate("__bool__") __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") class ImageFileDirectory_v2(MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. Exposes a dictionary interface of the tags in the directory:: ifd = ImageFileDirectory_v2() ifd[key] = 'Some Data' ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) 'Some Data' Individual values are returned as the strings or numbers, sequences are returned as tuples of the values. The tiff metadata type of each item is stored in a dictionary of tag types in :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types are read from a tiff file, guessed from the type added, or added manually. Data Structures: * self.tagtype = {} * Key: numerical tiff tag number * Value: integer corresponding to the data type from ~PIL.TiffTags.TYPES` .. versionadded:: 3.0.0 """ """ Documentation: 'internal' data structures: * self._tags_v2 = {} Key: numerical tiff tag number Value: decoded data, as tuple for multiple values * self._tagdata = {} Key: numerical tiff tag number Value: undecoded byte string from file * self._tags_v1 = {} Key: numerical tiff tag number Value: decoded data in the v1 format Tags will be found in the private attributes self._tagdata, and in self._tags_v2 once decoded. self.legacy_api is a value for internal use, and shouldn't be changed from outside code. In cooperation with the ImageFileDirectory_v1 class, if legacy_api is true, then decoded tags will be populated into both _tags_v1 and _tags_v2. _tags_v2 will be used if this IFD is used in the TIFF save routine. Tags should be read from _tags_v1 if legacy_api == true. """ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): """Initialize an ImageFileDirectory. To construct an ImageFileDirectory from a real file, pass the 8-byte magic header to the constructor. To only set the endianness, pass it as the 'prefix' keyword argument. :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets endianness. :param prefix: Override the endianness of the file. """ if ifh[:4] not in PREFIXES: raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: self._endian = ">" elif self._prefix == II: self._endian = "<" else: raise SyntaxError("not a TIFF IFD") self.tagtype = {} """ Dictionary of tag types """ self.reset() (self.next,) = self._unpack("L", ifh[4:]) self._legacy_api = False prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) legacy_api = property(lambda self: self._legacy_api) @legacy_api.setter def legacy_api(self, value): raise Exception("Not allowing setting of legacy api") def reset(self): self._tags_v1 = {} # will remain empty if legacy_api is false self._tags_v2 = {} # main tag storage self._tagdata = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None self._offset = None def __str__(self): return str(dict(self)) def named(self): """ :returns: dict of name|key: value Returns the complete tag dictionary, with named tags where possible. """ return {TiffTags.lookup(code).name: value for code, value in self.items()} def __len__(self): return len(set(self._tagdata) | set(self._tags_v2)) def __getitem__(self, tag): if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] self[tag] = handler(self, data, self.legacy_api) # check type val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): val = (val,) return val def __contains__(self, tag): return tag in self._tags_v2 or tag in self._tagdata def __setitem__(self, tag, value): self._setitem(tag, value, self.legacy_api) def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) info = TiffTags.lookup(tag) values = [value] if isinstance(value, basetypes) else value if tag not in self.tagtype: if info.type: self.tagtype[tag] = info.type else: self.tagtype[tag] = TiffTags.UNDEFINED if all(isinstance(v, IFDRational) for v in values): self.tagtype[tag] = ( TiffTags.RATIONAL if all(v >= 0 for v in values) else TiffTags.SIGNED_RATIONAL ) elif all(isinstance(v, int) for v in values): if all(0 <= v < 2 ** 16 for v in values): self.tagtype[tag] = TiffTags.SHORT elif all(-(2 ** 15) < v < 2 ** 15 for v in values): self.tagtype[tag] = TiffTags.SIGNED_SHORT else: self.tagtype[tag] = ( TiffTags.LONG if all(v >= 0 for v in values) else TiffTags.SIGNED_LONG ) elif all(isinstance(v, float) for v in values): self.tagtype[tag] = TiffTags.DOUBLE elif all(isinstance(v, str) for v in values): self.tagtype[tag] = TiffTags.ASCII elif all(isinstance(v, bytes) for v in values): self.tagtype[tag] = TiffTags.BYTE if self.tagtype[tag] == TiffTags.UNDEFINED: values = [ v.encode("ascii", "replace") if isinstance(v, str) else v for v in values ] elif self.tagtype[tag] == TiffTags.RATIONAL: values = [float(v) if isinstance(v, int) else v for v in values] is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict) if not is_ifd: values = tuple(info.cvt_enum(value) for value in values) dest = self._tags_v1 if legacy_api else self._tags_v2 # Three branches: # Spec'd length == 1, Actual length 1, store as element # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed. # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple. # Don't mess with the legacy api, since it's frozen. if not is_ifd and ( (info.length == 1) or self.tagtype[tag] == TiffTags.BYTE or (info.length is None and len(values) == 1 and not legacy_api) ): # Don't mess with the legacy api, since it's frozen. if legacy_api and self.tagtype[tag] in [ TiffTags.RATIONAL, TiffTags.SIGNED_RATIONAL, ]: # rationals values = (values,) try: (dest[tag],) = values except ValueError: # We've got a builtin tag with 1 expected entry warnings.warn( f"Metadata Warning, tag {tag} had too many entries: " f"{len(values)}, expected 1" ) dest[tag] = values[0] else: # Spec'd length > 1 or undefined # Unspec'd, and length > 1 dest[tag] = values def __delitem__(self, tag): self._tags_v2.pop(tag, None) self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v2)) def _unpack(self, fmt, data): return struct.unpack(self._endian + fmt, data) def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) def _register_loader(idx, size): def decorator(func): from .TiffTags import TYPES if func.__name__.startswith("load_"): TYPES[idx] = func.__name__[5:].replace("_", " ") _load_dispatch[idx] = size, func # noqa: F821 return func return decorator def _register_writer(idx): def decorator(func): _write_dispatch[idx] = func # noqa: F821 return func return decorator def _register_basic(idx_fmt_name): from .TiffTags import TYPES idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = ( # noqa: F821 size, lambda self, data, legacy_api=True: ( self._unpack("{}{}".format(len(data) // size, fmt), data) ), ) _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) list( map( _register_basic, [ (TiffTags.SHORT, "H", "short"), (TiffTags.LONG, "L", "long"), (TiffTags.SIGNED_BYTE, "b", "signed byte"), (TiffTags.SIGNED_SHORT, "h", "signed short"), (TiffTags.SIGNED_LONG, "l", "signed long"), (TiffTags.FLOAT, "f", "float"), (TiffTags.DOUBLE, "d", "double"), (TiffTags.IFD, "L", "long"), ], ) ) @_register_loader(1, 1) # Basic type, except for the legacy API. def load_byte(self, data, legacy_api=True): return data @_register_writer(1) # Basic type, except for the legacy API. def write_byte(self, data): return data @_register_loader(2, 1) def load_string(self, data, legacy_api=True): if data.endswith(b"\0"): data = data[:-1] return data.decode("latin-1", "replace") @_register_writer(2) def write_string(self, value): # remerge of https://github.com/python-pillow/Pillow/pull/1416 return b"" + value.encode("ascii", "replace") + b"\0" @_register_loader(5, 8) def load_rational(self, data, legacy_api=True): vals = self._unpack("{}L".format(len(data) // 4), data) def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) def write_rational(self, *values): return b"".join( self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values ) @_register_loader(7, 1) def load_undefined(self, data, legacy_api=True): return data @_register_writer(7) def write_undefined(self, value): return value @_register_loader(10, 8) def load_signed_rational(self, data, legacy_api=True): vals = self._unpack("{}l".format(len(data) // 4), data) def combine(a, b): return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) def write_signed_rational(self, *values): return b"".join( self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31))) for frac in values ) def _ensure_read(self, fp, size): ret = fp.read(size) if len(ret) != size: raise OSError( "Corrupt EXIF data. " f"Expecting to read {size} bytes but only got {len(ret)}. " ) return ret def load(self, fp): self.reset() self._offset = fp.tell() try: for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]): tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12)) tagname = TiffTags.lookup(tag).name typname = TYPES.get(typ, "unknown") msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})" try: unit_size, handler = self._load_dispatch[typ] except KeyError: logger.debug(msg + f" - unsupported type {typ}") continue # ignore unsupported type size = count * unit_size if size > 4: here = fp.tell() (offset,) = self._unpack("L", data) msg += f" Tag Location: {here} - Data Location: {offset}" fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) else: data = data[:size] if len(data) != size: warnings.warn( "Possibly corrupt EXIF data. " f"Expecting to read {size} bytes but only got {len(data)}." f" Skipping tag {tag}" ) logger.debug(msg) continue if not data: logger.debug(msg) continue self._tagdata[tag] = data self.tagtype[tag] = typ msg += " - value: " + ( "" % size if size > 32 else repr(data) ) logger.debug(msg) (self.next,) = self._unpack("L", self._ensure_read(fp, 4)) except OSError as msg: warnings.warn(str(msg)) return def tobytes(self, offset=0): # FIXME What about tagdata? result = self._pack("H", len(self._tags_v2)) entries = [] offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) typ = self.tagtype.get(tag) logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}") is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: if self._endian == "<": ifh = b"II\x2A\x00\x08\x00\x00\x00" else: ifh = b"MM\x00\x2A\x00\x00\x00\x08" ifd = ImageFileDirectory_v2(ifh) for ifd_tag, ifd_value in self._tags_v2[tag].items(): ifd[ifd_tag] = ifd_value data = ifd.tobytes(offset) else: values = value if isinstance(value, tuple) else (value,) data = self._write_dispatch[typ](self, *values) tagname = TiffTags.lookup(tag).name typname = "ifd" if is_ifd else TYPES.get(typ, "unknown") msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})" msg += " - value: " + ( "" % len(data) if len(data) >= 16 else str(values) ) logger.debug(msg) # count is sum of lengths for string and arbitrary data if is_ifd: count = 1 elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]: count = len(data) else: count = len(values) # figure out if data fits into the entry if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: entries.append((tag, typ, count, self._pack("L", offset), data)) offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data if stripoffsets is not None: tag, typ, count, value, data = entries[stripoffsets] if data: raise NotImplementedError("multistrip support not yet implemented") value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}") result += self._pack("HHL4s", tag, typ, count, value) # -- overwrite here for multi-page -- result += b"\0\0\0\0" # end of entries # pass 3: write auxiliary data to file for tag, typ, count, value, data in entries: result += data if len(data) & 1: result += b"\0" return result def save(self, fp): if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) offset = fp.tell() result = self.tobytes(offset) fp.write(result) return offset + len(result) ImageFileDirectory_v2._load_dispatch = _load_dispatch ImageFileDirectory_v2._write_dispatch = _write_dispatch for idx, name in TYPES.items(): name = name.replace(" ", "_") setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) del _load_dispatch, _write_dispatch, idx, name # Legacy ImageFileDirectory support. class ImageFileDirectory_v1(ImageFileDirectory_v2): """This class represents the **legacy** interface to a TIFF tag directory. Exposes a dictionary interface of the tags in the directory:: ifd = ImageFileDirectory_v1() ifd[key] = 'Some Data' ifd.tagtype[key] = TiffTags.ASCII print(ifd[key]) ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. Values are returned as a tuple. .. deprecated:: 3.0.0 """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._legacy_api = True tags = property(lambda self: self._tags_v1) tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 tagtype: dict """Dictionary of tag types""" @classmethod def from_v2(cls, original): """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance. :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` """ ifd = cls(prefix=original.prefix) ifd._tagdata = original._tagdata ifd.tagtype = original.tagtype ifd.next = original.next # an indicator for multipage tiffs return ifd def to_v2(self): """Returns an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` instance with the same data as is contained in the original :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` instance. :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` """ ifd = ImageFileDirectory_v2(prefix=self.prefix) ifd._tagdata = dict(self._tagdata) ifd.tagtype = dict(self.tagtype) ifd._tags_v2 = dict(self._tags_v2) return ifd def __contains__(self, tag): return tag in self._tags_v1 or tag in self._tagdata def __len__(self): return len(set(self._tagdata) | set(self._tags_v1)) def __iter__(self): return iter(set(self._tagdata) | set(self._tags_v1)) def __setitem__(self, tag, value): for legacy_api in (False, True): self._setitem(tag, value, legacy_api) def __getitem__(self, tag): if tag not in self._tags_v1: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] for legacy in (False, True): self._setitem(tag, handler(self, data, legacy), legacy) val = self._tags_v1[tag] if not isinstance(val, (tuple, bytes)): val = (val,) return val # undone -- switch this pointer when IFD_LEGACY_API == False ImageFileDirectory = ImageFileDirectory_v1 ## # Image plugin for TIFF files. class TiffImageFile(ImageFile.ImageFile): format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False def __init__(self, fp=None, filename=None): self.tag_v2 = None """ Image file directory (tag dictionary) """ self.tag = None """ Legacy tag entries """ super().__init__(fp, filename) def _open(self): """Open the first image in a TIFF file""" # Header ifh = self.fp.read(8) self.tag_v2 = ImageFileDirectory_v2(ifh) # legacy IFD entries will be filled in later self.ifd = None # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 self.__fp = self.fp self._frame_pos = [] self._n_frames = None logger.debug("*** TiffImageFile._open ***") logger.debug(f"- __first: {self.__first}") logger.debug(f"- ifh: {repr(ifh)}") # Use repr to avoid str(bytes) # and load the first frame self._seek(0) @property def n_frames(self): if self._n_frames is None: current = self.tell() self._seek(len(self._frame_pos)) while self._n_frames is None: self._seek(self.tell() + 1) self.seek(current) return self._n_frames def seek(self, frame): """Select a given frame as current image""" if not self._seek_check(frame): return self._seek(frame) # Create a new core image object on second and # subsequent frames in the image. Image may be # different size/mode. Image._decompression_bomb_check(self.size) self.im = Image.core.new(self.mode, self.size) def _seek(self, frame): self.fp = self.__fp while len(self._frame_pos) <= frame: if not self.__next: raise EOFError("no more images in TIFF file") logger.debug( f"Seeking to frame {frame}, on frame {self.__frame}, " f"__next {self.__next}, location: {self.fp.tell()}" ) # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer self.fp.tell() self.fp.seek(self.__next) self._frame_pos.append(self.__next) logger.debug("Loading tags, location: %s" % self.fp.tell()) self.tag_v2.load(self.fp) self.__next = self.tag_v2.next if self.__next == 0: self._n_frames = frame + 1 if len(self._frame_pos) == 1: self.is_animated = self.__next != 0 self.__frame += 1 self.fp.seek(self._frame_pos[frame]) self.tag_v2.load(self.fp) # fill the legacy tag/ifd entries self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame self._setup() def tell(self): """Return the current frame number""" return self.__frame def load(self): if self.tile and self.use_load_libtiff: return self._load_libtiff() return super().load() def load_end(self): if self._tile_orientation: method = { 2: Image.FLIP_LEFT_RIGHT, 3: Image.ROTATE_180, 4: Image.FLIP_TOP_BOTTOM, 5: Image.TRANSPOSE, 6: Image.ROTATE_270, 7: Image.TRANSVERSE, 8: Image.ROTATE_90, }.get(self._tile_orientation) if method is not None: self.im = self.im.transpose(method) self._size = self.im.size # allow closing if we're on the first frame, there's no next # This is the ImageFile.load path only, libtiff specific below. if not self.is_animated: self._close_exclusive_fp_after_loading = True def _load_libtiff(self): """Overload method triggered when we detect a compressed tiff Calls out to libtiff""" Image.Image.load(self) self.load_prepare() if not len(self.tile) == 1: raise OSError("Not exactly one tile") # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] args = list(self.tile[0][3]) # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. # libtiff closes the file descriptor, so pass in a dup. try: fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell # in _seek if hasattr(self.fp, "flush"): self.fp.flush() except OSError: # io.BytesIO have a fileno, but returns an OSError if # it doesn't use a file descriptor. fp = False if fp: args[2] = fp decoder = Image._getdecoder( self.mode, "libtiff", tuple(args), self.decoderconfig ) try: decoder.setimage(self.im, extents) except ValueError as e: raise OSError("Couldn't set the image") from e close_self_fp = self._exclusive_fp and not self.is_animated if hasattr(self.fp, "getvalue"): # We've got a stringio like thing passed in. Yay for all in memory. # The decoder needs the entire file in one shot, so there's not # a lot we can do here other than give it the entire file. # unless we could do something like get the address of the # underlying string for stringio. # # Rearranging for supporting byteio items, since they have a fileno # that returns an OSError if there's no underlying fp. Easier to # deal with here by reordering. logger.debug("have getvalue. just sending in a string from getvalue") n, err = decoder.decode(self.fp.getvalue()) elif fp: # we've got a actual file on disk, pass in the fp. logger.debug("have fileno, calling fileno version of the decoder.") if not close_self_fp: self.fp.seek(0) # 4 bytes, otherwise the trace might error out n, err = decoder.decode(b"fpfp") else: # we have something else. logger.debug("don't have fileno or getvalue. just reading") self.fp.seek(0) # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) self.tile = [] self.readonly = 0 self.load_end() # libtiff closed the fp in a, we need to close self.fp, if possible if close_self_fp: self.fp.close() self.fp = None # might be shared if err < 0: raise OSError(err) return Image.Image.load(self) def _setup(self): """Setup this image object based on current tags""" if 0xBC01 in self.tag_v2: raise OSError("Windows Media Photo files not yet supported") # extract relevant tags self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) # photometric is a required tag, but not everyone is reading # the specification photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) # old style jpeg compression images most certainly are YCbCr if self._compression == "tiff_jpeg": photo = 6 fillorder = self.tag_v2.get(FILLORDER, 1) logger.debug("*** Summary ***") logger.debug(f"- compression: {self._compression}") logger.debug(f"- photometric_interpretation: {photo}") logger.debug(f"- planar_configuration: {self._planar_configuration}") logger.debug(f"- fill_order: {fillorder}") logger.debug(f"- YCbCr subsampling: {self.tag.get(530)}") # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) ysize = int(self.tag_v2.get(IMAGELENGTH)) self._size = xsize, ysize logger.debug(f"- size: {self.size}") sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,)) if len(sampleFormat) > 1 and max(sampleFormat) == min(sampleFormat) == 1: # SAMPLEFORMAT is properly per band, so an RGB image will # be (1,1,1). But, we don't support per band pixel types, # and anything more than one band is a uint8. So, just # take the first element. Revisit this if adding support # for more exotic images. sampleFormat = (1,) bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,)) extra_tuple = self.tag_v2.get(EXTRASAMPLES, ()) if photo in (2, 6, 8): # RGB, YCbCr, LAB bps_count = 3 elif photo == 5: # CMYK bps_count = 4 else: bps_count = 1 bps_count += len(extra_tuple) # Some files have only one value in bps_tuple, # while should have more. Fix it if bps_count > len(bps_tuple) and len(bps_tuple) == 1: bps_tuple = bps_tuple * bps_count samplesPerPixel = self.tag_v2.get(SAMPLESPERPIXEL, 1) if len(bps_tuple) != samplesPerPixel: raise SyntaxError("unknown data organization") # mode: check photometric interpretation and bits per pixel key = ( self.tag_v2.prefix, photo, sampleFormat, fillorder, bps_tuple, extra_tuple, ) logger.debug(f"format key: {key}") try: self.mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") raise SyntaxError("unknown pixel mode") from e logger.debug(f"- raw mode: {rawmode}") logger.debug(f"- pil mode: {self.mode}") self.info["compression"] = self._compression xres = self.tag_v2.get(X_RESOLUTION, 1) yres = self.tag_v2.get(Y_RESOLUTION, 1) if xres and yres: resunit = self.tag_v2.get(RESOLUTION_UNIT) if resunit == 2: # dots per inch self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) elif resunit == 3: # dots per centimeter. convert to dpi self.info["dpi"] = int(xres * 2.54 + 0.5), int(yres * 2.54 + 0.5) elif resunit is None: # used to default to 1, but now 2) self.info["dpi"] = int(xres + 0.5), int(yres + 0.5) # For backward compatibility, # we also preserve the old behavior self.info["resolution"] = xres, yres else: # No absolute unit of measurement self.info["resolution"] = xres, yres # build tile descriptors x = y = layer = 0 self.tile = [] self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw" if self.use_load_libtiff: # Decoder expects entire file as one tile. # There's a buffer size limit in load (64k) # so large g4 images will fail if we use that # function. # # Setup the one tile for the whole image, then # use the _load_libtiff function. # libtiff handles the fillmode for us, so 1;IR should # actually be 1;I. Including the R double reverses the # bits, so stripes of the image are reversed. See # https://github.com/python-pillow/Pillow/issues/279 if fillorder == 2: # Replace fillorder with fillorder=1 key = key[:3] + (1,) + key[4:] logger.debug(f"format key: {key}") # this should always work, since all the # fillorder==2 modes have a corresponding # fillorder=1 mode self.mode, rawmode = OPEN_INFO[key] # libtiff always returns the bytes in native order. # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. if rawmode == "I;16": rawmode = "I;16N" if ";16B" in rawmode: rawmode = rawmode.replace(";16B", ";16N") if ";16L" in rawmode: rawmode = rawmode.replace(";16L", ";16N") # YCbCr images with new jpeg compression with pixels in one plane # unpacked straight into RGB values if ( photo == 6 and self._compression == "jpeg" and self._planar_configuration == 1 ): rawmode = "RGB" # Offset in the tile tuple is 0, we go from 0,0 to # w,h, and we only do this once -- eds a = (rawmode, self._compression, False, self.tag_v2.offset) self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a)) elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2: # striped image if STRIPOFFSETS in self.tag_v2: offsets = self.tag_v2[STRIPOFFSETS] h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] else: # tiled image offsets = self.tag_v2[TILEOFFSETS] w = self.tag_v2.get(322) h = self.tag_v2.get(323) for offset in offsets: if x + w > xsize: stride = w * sum(bps_tuple) / 8 # bytes per line else: stride = 0 tile_rawmode = rawmode if self._planar_configuration == 2: # each band on it's own layer tile_rawmode = rawmode[layer] # adjust stride width accordingly stride /= bps_count a = (tile_rawmode, int(stride), 1) self.tile.append( ( self._compression, (x, y, min(x + w, xsize), min(y + h, ysize)), offset, a, ) ) x = x + w if x >= self.size[0]: x, y = 0, y + h if y >= self.size[1]: x = y = 0 layer += 1 else: logger.debug("- unsupported data organization") raise SyntaxError("unknown data organization") # Fix up info. if ICCPROFILE in self.tag_v2: self.info["icc_profile"] = self.tag_v2[ICCPROFILE] # fixup palette descriptor if self.mode in ["P", "PA"]: palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) self._tile_orientation = self.tag_v2.get(0x0112) def _close__fp(self): try: if self.__fp != self.fp: self.__fp.close() except AttributeError: pass finally: self.__fp = None # # -------------------------------------------------------------------- # Write TIFF files # little endian is default except for image modes with # explicit big endian byte-order SAVE_INFO = { # mode => rawmode, byteorder, photometrics, # sampleformat, bitspersample, extra "1": ("1", II, 1, 1, (1,), None), "L": ("L", II, 1, 1, (8,), None), "LA": ("LA", II, 1, 1, (8, 8), 2), "P": ("P", II, 3, 1, (8,), None), "PA": ("PA", II, 3, 1, (8, 8), 2), "I": ("I;32S", II, 1, 2, (32,), None), "I;16": ("I;16", II, 1, 1, (16,), None), "I;16S": ("I;16S", II, 1, 2, (16,), None), "F": ("F;32F", II, 1, 3, (32,), None), "RGB": ("RGB", II, 2, 1, (8, 8, 8), None), "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0), "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2), "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None), "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None), "LAB": ("LAB", II, 8, 1, (8, 8, 8), None), "I;32BS": ("I;32BS", MM, 1, 2, (32,), None), "I;16B": ("I;16B", MM, 1, 1, (16,), None), "I;16BS": ("I;16BS", MM, 1, 2, (16,), None), "F;32BF": ("F;32BF", MM, 1, 3, (32,), None), } def _save(im, fp, filename): try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: raise OSError(f"cannot write mode {im.mode} as TIFF") from e ifd = ImageFileDirectory_v2(prefix=prefix) compression = im.encoderinfo.get("compression", im.info.get("compression")) if compression is None: compression = "raw" elif compression == "tiff_jpeg": # OJPEG is obsolete, so use new-style JPEG compression instead compression = "jpeg" elif compression == "tiff_deflate": compression = "tiff_adobe_deflate" libtiff = WRITE_LIBTIFF or compression != "raw" # required for color libtiff images ifd[PLANAR_CONFIGURATION] = getattr(im, "_planar_configuration", 1) ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] # write any arbitrary tags passed in as an ImageFileDirectory info = im.encoderinfo.get("tiffinfo", {}) logger.debug("Tiffinfo Keys: %s" % list(info)) if isinstance(info, ImageFileDirectory_v1): info = info.to_v2() for key in info: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] except Exception: pass # might not be an IFD. Might not have populated type # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com if hasattr(im, "tag_v2"): # preserve tags from original TIFF image file for key in ( RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP, ): if key in im.tag_v2: ifd[key] = im.tag_v2[key] ifd.tagtype[key] = im.tag_v2.tagtype[key] # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech icc = im.encoderinfo.get("icc_profile", im.info.get("icc_profile")) if icc: ifd[ICCPROFILE] = icc for key, name in [ (IMAGEDESCRIPTION, "description"), (X_RESOLUTION, "resolution"), (Y_RESOLUTION, "resolution"), (X_RESOLUTION, "x_resolution"), (Y_RESOLUTION, "y_resolution"), (RESOLUTION_UNIT, "resolution_unit"), (SOFTWARE, "software"), (DATE_TIME, "date_time"), (ARTIST, "artist"), (COPYRIGHT, "copyright"), ]: if name in im.encoderinfo: ifd[key] = im.encoderinfo[name] dpi = im.encoderinfo.get("dpi") if dpi: ifd[RESOLUTION_UNIT] = 2 ifd[X_RESOLUTION] = int(dpi[0] + 0.5) ifd[Y_RESOLUTION] = int(dpi[1] + 0.5) if bits != (1,): ifd[BITSPERSAMPLE] = bits if len(bits) != 1: ifd[SAMPLESPERPIXEL] = len(bits) if extra is not None: ifd[EXTRASAMPLES] = extra if format != 1: ifd[SAMPLEFORMAT] = format ifd[PHOTOMETRIC_INTERPRETATION] = photo if im.mode in ["P", "PA"]: lut = im.im.getpalette("RGB", "RGB;L") ifd[COLORMAP] = tuple(v * 256 for v in lut) # data orientation stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8) ifd[ROWSPERSTRIP] = im.size[1] strip_byte_counts = stride * im.size[1] if strip_byte_counts >= 2 ** 16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = strip_byte_counts ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer # no compression by default: ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if libtiff: if "quality" in im.encoderinfo: quality = im.encoderinfo["quality"] if not isinstance(quality, int) or quality < 0 or quality > 100: raise ValueError("Invalid quality setting") if compression != "jpeg": raise ValueError( "quality setting only supported for 'jpeg' compression" ) ifd[JPEGQUALITY] = quality logger.debug("Saving using libtiff encoder") logger.debug("Items: %s" % sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: fp.seek(0) _fp = os.dup(fp.fileno()) except io.UnsupportedOperation: pass # optional types for non core tags types = {} # SAMPLEFORMAT is determined by the image format and should not be copied # from legacy_ifd. # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library # based on the data in the strip. # The other tags expect arrays with a certain length (fixed or depending on # BITSPERSAMPLE, etc), passing arrays with a different length will result in # segfaults. Block these tags until we add extra validation. # SUBIFD may also cause a segfault. blocklist = [ REFERENCEBLACKWHITE, SAMPLEFORMAT, STRIPBYTECOUNTS, STRIPOFFSETS, TRANSFERFUNCTION, SUBIFD, ] atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. legacy_ifd = {} if hasattr(im, "tag"): legacy_ifd = im.tag.to_v2() for tag, value in itertools.chain( ifd.items(), getattr(im, "tag_v2", {}).items(), legacy_ifd.items() ): # Libtiff can only process certain core items without adding # them to the custom dictionary. # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: if not Image.core.libtiff_support_custom_tags: continue if tag in ifd.tagtype: types[tag] = ifd.tagtype[tag] elif not (isinstance(value, (int, float, str, bytes))): continue else: type = TiffTags.lookup(tag).type if type: types[tag] = type if tag not in atts and tag not in blocklist: if isinstance(value, str): atts[tag] = value.encode("ascii", "replace") + b"\0" elif isinstance(value, IFDRational): atts[tag] = float(value) else: atts[tag] = value logger.debug("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode # contains I;16, we need to convert from native to image # byte order. if im.mode in ("I;16B", "I;16"): rawmode = "I;16N" # Pass tags as sorted list so that the tags are set in a fixed order. # This is required by libtiff for some tags. For example, the JPEGQUALITY # pseudo tag requires that the COMPRESS tag was already set. tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) e = Image._getencoder(im.mode, "libtiff", a, im.encoderconfig) e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: l, s, d = e.encode(16 * 1024) if not _fp: fp.write(d) if s: break if s < 0: raise OSError(f"encoder error {s} when writing image file") else: offset = ifd.save(fp) ImageFile._save( im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] ) # -- helper for multi-page save -- if "_debug_multipage" in im.encoderinfo: # just to access o32 and o16 (using correct byte order) im._debug_multipage = ifd class AppendingTiffWriter: fieldSizes = [ 0, # None 1, # byte 1, # ascii 2, # short 4, # long 8, # rational 1, # sbyte 1, # undefined 2, # sshort 4, # slong 8, # srational 4, # float 8, # double ] # StripOffsets = 273 # FreeOffsets = 288 # TileOffsets = 324 # JPEGQTables = 519 # JPEGDCTables = 520 # JPEGACTables = 521 Tags = {273, 288, 324, 519, 520, 521} def __init__(self, fn, new=False): if hasattr(fn, "read"): self.f = fn self.close_fp = False else: self.name = fn self.close_fp = True try: self.f = open(fn, "w+b" if new else "r+b") except OSError: self.f = open(fn, "w+b") self.beginning = self.f.tell() self.setup() def setup(self): # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) self.whereToWriteNewIFDOffset = None self.offsetOfNewPage = 0 self.IIMM = IIMM = self.f.read(4) if not IIMM: # empty file - first page self.isFirst = True return self.isFirst = False if IIMM == b"II\x2a\x00": self.setEndian("<") elif IIMM == b"MM\x00\x2a": self.setEndian(">") else: raise RuntimeError("Invalid TIFF file header") self.skipIFDs() self.goToEnd() def finalize(self): if self.isFirst: return # fix offsets self.f.seek(self.offsetOfNewPage) IIMM = self.f.read(4) if not IIMM: # raise RuntimeError("nothing written into new page") # Make it easy to finish a frame without committing to a new one. return if IIMM != self.IIMM: raise RuntimeError("IIMM of new page doesn't match IIMM of first page") IFDoffset = self.readLong() IFDoffset += self.offsetOfNewPage self.f.seek(self.whereToWriteNewIFDOffset) self.writeLong(IFDoffset) self.f.seek(IFDoffset) self.fixIFD() def newFrame(self): # Call this to finish a frame. self.finalize() self.setup() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): if self.close_fp: self.close() return False def tell(self): return self.f.tell() - self.offsetOfNewPage def seek(self, offset, whence=io.SEEK_SET): if whence == os.SEEK_SET: offset += self.offsetOfNewPage self.f.seek(offset, whence) return self.tell() def goToEnd(self): self.f.seek(0, os.SEEK_END) pos = self.f.tell() # pad to 16 byte boundary padBytes = 16 - pos % 16 if 0 < padBytes < 16: self.f.write(bytes(padBytes)) self.offsetOfNewPage = self.f.tell() def setEndian(self, endian): self.endian = endian self.longFmt = self.endian + "L" self.shortFmt = self.endian + "H" self.tagFormat = self.endian + "HHL" def skipIFDs(self): while True: IFDoffset = self.readLong() if IFDoffset == 0: self.whereToWriteNewIFDOffset = self.f.tell() - 4 break self.f.seek(IFDoffset) numTags = self.readShort() self.f.seek(numTags * 12, os.SEEK_CUR) def write(self, data): return self.f.write(data) def readShort(self): (value,) = struct.unpack(self.shortFmt, self.f.read(2)) return value def readLong(self): (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value def rewriteLastShortToLong(self, value): self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def rewriteLastShort(self, value): self.f.seek(-2, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") def rewriteLastLong(self, value): self.f.seek(-4, os.SEEK_CUR) bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def writeShort(self, value): bytesWritten = self.f.write(struct.pack(self.shortFmt, value)) if bytesWritten is not None and bytesWritten != 2: raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 2") def writeLong(self, value): bytesWritten = self.f.write(struct.pack(self.longFmt, value)) if bytesWritten is not None and bytesWritten != 4: raise RuntimeError(f"wrote only {bytesWritten} bytes but wanted 4") def close(self): self.finalize() self.f.close() def fixIFD(self): numTags = self.readShort() for i in range(numTags): tag, fieldType, count = struct.unpack(self.tagFormat, self.f.read(8)) fieldSize = self.fieldSizes[fieldType] totalSize = fieldSize * count isLocal = totalSize <= 4 if not isLocal: offset = self.readLong() offset += self.offsetOfNewPage self.rewriteLastLong(offset) if tag in self.Tags: curPos = self.f.tell() if isLocal: self.fixOffsets( count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) ) self.f.seek(curPos + 4) else: self.f.seek(offset) self.fixOffsets( count, isShort=(fieldSize == 2), isLong=(fieldSize == 4) ) self.f.seek(curPos) offset = curPos = None elif isLocal: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR) def fixOffsets(self, count, isShort=False, isLong=False): if not isShort and not isLong: raise RuntimeError("offset is neither short nor long") for i in range(count): offset = self.readShort() if isShort else self.readLong() offset += self.offsetOfNewPage if isShort and offset >= 65536: # offset is now too large - we must convert shorts to longs if count != 1: raise RuntimeError("not implemented") # XXX TODO # simple case - the offset is just one and therefore it is # local (not referenced with another offset) self.rewriteLastShortToLong(offset) self.f.seek(-10, os.SEEK_CUR) self.writeShort(TiffTags.LONG) # rewrite the type to LONG self.f.seek(8, os.SEEK_CUR) elif isShort: self.rewriteLastShort(offset) else: self.rewriteLastLong(offset) def _save_all(im, fp, filename): encoderinfo = im.encoderinfo.copy() encoderconfig = im.encoderconfig append_images = list(encoderinfo.get("append_images", [])) if not hasattr(im, "n_frames") and not append_images: return _save(im, fp, filename) cur_idx = im.tell() try: with AppendingTiffWriter(fp) as tf: for ims in [im] + append_images: ims.encoderinfo = encoderinfo ims.encoderconfig = encoderconfig if not hasattr(ims, "n_frames"): nfr = 1 else: nfr = ims.n_frames for idx in range(nfr): ims.seek(idx) ims.load() _save(ims, tf, filename) tf.newFrame() finally: im.seek(cur_idx) # # -------------------------------------------------------------------- # Register Image.register_open(TiffImageFile.format, TiffImageFile, _accept) Image.register_save(TiffImageFile.format, _save) Image.register_save_all(TiffImageFile.format, _save_all) Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"]) Image.register_mime(TiffImageFile.format, "image/tiff")