Pillow/PIL/TiffImagePlugin.py

1820 lines
63 KiB
Python
Raw Normal View History

2010-07-31 06:52:47 +04:00
#
# 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.
#
from __future__ import division, print_function
from . import Image, ImageFile, ImagePalette, TiffTags
from ._binary import i8, o8
2010-07-31 06:52:47 +04:00
import collections
from fractions import Fraction
from numbers import Number, Rational
import io
import itertools
import os
import struct
import sys
import warnings
2016-02-05 01:57:13 +03:00
from .TiffTags import TYPES
2010-07-31 06:52:47 +04:00
2015-08-25 15:27:18 +03:00
__version__ = "1.3.5"
DEBUG = False # Needs to be merged with the new logging approach.
# Set these to true to force use of libtiff for reading or writing.
2013-11-22 08:33:16 +04:00
READ_LIBTIFF = False
2014-07-28 20:00:06 +04:00
WRITE_LIBTIFF = False
2015-09-10 18:49:37 +03:00
IFD_LEGACY_API = True
2013-11-22 08:33:16 +04:00
2014-07-28 20:00:06 +04:00
II = b"II" # little-endian (Intel style)
MM = b"MM" # big-endian (Motorola style)
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
# 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
SOFTWARE = 305
DATE_TIME = 306
ARTIST = 315
PREDICTOR = 317
COLORMAP = 320
TILEOFFSETS = 324
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
COPYRIGHT = 33432
2014-07-28 20:00:06 +04:00
IPTC_NAA_CHUNK = 33723 # newsphoto properties
PHOTOSHOP_CHUNK = 34377 # photoshop properties
2010-07-31 06:52:47 +04:00
ICCPROFILE = 34675
EXIFIFD = 34665
XMP = 700
2015-09-28 14:53:25 +03:00
# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
2013-09-27 21:41:27 +04:00
IMAGEJ_META_DATA_BYTE_COUNTS = 50838
IMAGEJ_META_DATA = 50839
2010-07-31 06:52:47 +04:00
COMPRESSION_INFO = {
# Compression => pil compression name
1: "raw",
2: "tiff_ccitt",
3: "group3",
4: "group4",
5: "tiff_lzw",
2014-07-28 20:00:06 +04:00
6: "tiff_jpeg", # obsolete
2010-07-31 06:52:47 +04:00
7: "jpeg",
2013-07-01 18:45:42 +04:00
8: "tiff_adobe_deflate",
2014-07-28 20:00:06 +04:00
32771: "tiff_raw_16", # 16-bit padding
2013-07-01 18:45:42 +04:00
32773: "packbits",
32809: "tiff_thunderscan",
32946: "tiff_deflate",
34676: "tiff_sgilog",
34677: "tiff_sgilog24",
2010-07-31 06:52:47 +04:00
}
COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}
2010-07-31 06:52:47 +04:00
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), (1,)): ("RGBA", "RGBa"),
(MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
(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), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
(MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10
2017-08-21 16:28:29 +03:00
(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, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
(MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"),
(II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
(MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
2010-07-31 06:52:47 +04:00
}
2017-08-21 17:09:35 +03:00
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
2017-08-21 17:14:33 +03:00
b"MM\x2A\x00", # Invalid TIFF header, assume big-endian
b"II\x00\x2A", # Invalid TIFF header, assume little-endian
2017-08-21 17:09:35 +03:00
]
2010-07-31 06:52:47 +04:00
2014-07-28 20:00:06 +04:00
2010-07-31 06:52:47 +04:00
def _accept(prefix):
return prefix[:4] in PREFIXES
2014-07-28 20:00:06 +04:00
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
2017-05-27 23:55:14 +03:00
2010-07-31 06:52:47 +04:00
##
# Wrapper for TIFF IFDs.
_load_dispatch = {}
_write_dispatch = {}
2016-02-05 01:57:13 +03:00
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
"""
2015-11-18 20:00:15 +03:00
self._denominator = denominator
self._numerator = value
self._val = float(1)
if isinstance(value, Fraction):
2015-11-18 20:00:15 +03:00
self._numerator = value.numerator
self._denominator = value.denominator
self._val = value
if isinstance(value, IFDRational):
2015-11-18 20:00:15 +03:00
self._denominator = value.denominator
self._numerator = value.numerator
self._val = value._val
return
if denominator == 0:
self._val = float('nan')
return
elif denominator == 1:
self._val = Fraction(value)
else:
self._val = Fraction(value, denominator)
2015-11-18 20:00:15 +03:00
@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))
2015-10-25 17:17:50 +03:00
2015-10-25 17:49:52 +03:00
def __hash__(self):
return self._val.__hash__()
2016-02-05 01:57:13 +03:00
def __eq__(self, other):
return self._val == other
def _delegate(op):
def delegate(self, *args):
2016-02-05 01:57:13 +03:00
return getattr(self._val, op)(*args)
return delegate
""" a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul',
'truediv', 'rtruediv', 'floordiv',
'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg',
2015-12-28 00:04:23 +03:00
'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero',
'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__')
__div__ = _delegate('__div__')
__rdiv__ = _delegate('__rdiv__')
__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__')
__nonzero__ = _delegate('__nonzero__')
2015-12-28 00:04:23 +03:00
__ceil__ = _delegate('__ceil__')
__floor__ = _delegate('__floor__')
__round__ = _delegate('__round__')
2015-10-25 17:49:52 +03:00
2015-09-11 20:09:14 +03:00
class ImageFileDirectory_v2(collections.MutableMapping):
"""This class represents a TIFF tag directory. To speed things up, we
don't decode tags unless they're asked for.
2015-09-14 17:01:57 +03:00
Exposes a dictionary interface of the tags in the directory::
2010-07-31 06:52:47 +04:00
2015-09-14 17:01:57 +03:00
ifd = ImageFileDirectory_v2()
ifd[key] = 'Some Data'
ifd.tagtype[key] = 2
print(ifd[key])
'Some Data'
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
Individual values are returned as the strings or numbers, sequences are
2015-09-22 13:31:59 +03:00
returned as tuples of the values.
2015-09-14 17:01:57 +03:00
The tiff metadata type of each item is stored in a dictionary of
tag types in
`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
are read from a tiff file, guessed from the type added, or added
manually.
Data Structures:
2015-09-14 17:01:57 +03:00
* self.tagtype = {}
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
* Key: numerical tiff tag number
* Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES`
.. versionadded:: 3.0.0
"""
"""
Documentation:
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
'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
2015-09-22 13:31:59 +03:00
Tags will be found in the private attributes self._tagdata, and in
self._tags_v2 once decoded.
2015-09-14 17:01:57 +03:00
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.
2015-09-22 13:31:59 +03:00
"""
2010-07-31 06:52:47 +04:00
2014-12-30 13:57:45 +03:00
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.
2015-09-14 17:01:57 +03:00
:param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
2014-12-30 13:57:45 +03:00
endianness.
2015-09-14 17:01:57 +03:00
:param prefix: Override the endianness of the file.
"""
2014-12-30 13:57:45 +03:00
if ifh[:4] not in PREFIXES:
raise SyntaxError("not a TIFF file (header %r not valid)" % ifh)
self._prefix = prefix if prefix is not None else ifh[:2]
if self._prefix == MM:
self._endian = ">"
2014-12-30 13:57:45 +03:00
elif self._prefix == II:
self._endian = "<"
2010-07-31 06:52:47 +04:00
else:
2014-12-30 13:57:45 +03:00
raise SyntaxError("not a TIFF IFD")
2010-07-31 06:52:47 +04:00
self.reset()
2014-12-30 13:57:45 +03:00
self.next, = self._unpack("L", ifh[4:])
2015-09-11 20:09:14 +03:00
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")
2010-07-31 06:52:47 +04:00
def reset(self):
2015-09-22 13:31:59 +03:00
self._tags_v1 = {} # will remain empty if legacy_api is false
self._tags_v2 = {} # main tag storage
self._tagdata = {}
2015-09-22 13:31:59 +03:00
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
2010-07-31 06:52:47 +04:00
def __str__(self):
return str(dict(self))
def named(self):
2014-07-28 20:00:06 +04:00
"""
2015-09-14 17:01:57 +03:00
:returns: dict of name|key: value
2015-09-22 13:31:59 +03:00
2015-05-29 07:59:54 +03:00
Returns the complete tag dictionary, with named tags where possible.
2014-07-28 20:00:06 +04:00
"""
return dict((TiffTags.lookup(code).name, value)
for code, value in self.items())
2010-07-31 06:52:47 +04:00
def __len__(self):
return len(set(self._tagdata) | set(self._tags_v2))
2010-07-31 06:52:47 +04:00
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
2010-07-31 06:52:47 +04:00
def __contains__(self, tag):
return tag in self._tags_v2 or tag in self._tagdata
if bytes is str:
def has_key(self, tag):
return tag in self
2010-07-31 06:52:47 +04:00
def __setitem__(self, tag, value):
self._setitem(tag, value, self.legacy_api)
def _setitem(self, tag, value, legacy_api):
basetypes = (Number, bytes, str)
if bytes is str:
basetypes += unicode,
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] = 7
if all(isinstance(v, IFDRational) for v in values):
self.tagtype[tag] = 5
elif all(isinstance(v, int) for v in values):
if all(v < 2 ** 16 for v in values):
self.tagtype[tag] = 3
else:
self.tagtype[tag] = 4
elif all(isinstance(v, float) for v in values):
self.tagtype[tag] = 12
else:
if bytes is str:
# Never treat data as binary by default on Python 2.
self.tagtype[tag] = 2
else:
if all(isinstance(v, str) for v in values):
self.tagtype[tag] = 2
if self.tagtype[tag] == 7 and bytes is not str:
2017-05-27 23:55:14 +03:00
values = [value.encode("ascii", 'replace') if isinstance(
value, str) else value]
values = tuple(info.cvt_enum(value) for value in values)
dest = self._tags_v1 if legacy_api else self._tags_v2
2015-09-22 13:31:59 +03:00
# 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 ((info.length == 1) 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 [5, 10]: # rationals
values = values,
try:
dest[tag], = values
except ValueError:
# We've got a builtin tag with 1 expected entry
warnings.warn(
"Metadata Warning, tag %s had too many entries: %s, expected 1" % (
tag, len(values)))
dest[tag] = values[0]
else:
# Spec'd length > 1 or undefined
# Unspec'd, and length > 1
dest[tag] = values
2010-07-31 06:52:47 +04:00
def __delitem__(self, tag):
self._tags_v2.pop(tag, None)
self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None)
2010-07-31 06:52:47 +04:00
def __iter__(self):
return iter(set(self._tagdata) | set(self._tags_v2))
2014-12-30 13:57:45 +03:00
def _unpack(self, fmt, data):
return struct.unpack(self._endian + fmt, data)
2014-12-30 13:57:45 +03:00
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
return func
return decorator
def _register_writer(idx):
def decorator(func):
_write_dispatch[idx] = func
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] = size, lambda self, data, legacy_api=True: (
self._unpack("{}{}".format(len(data) // size, fmt), data))
_write_dispatch[idx] = lambda self, *values: (
2014-12-30 13:57:45 +03:00
b"".join(self._pack(fmt, value) for value in values))
list(map(_register_basic,
2017-05-27 23:55:14 +03:00
[(3, "H", "short"),
(4, "L", "long"),
(6, "b", "signed byte"),
(8, "h", "signed short"),
(9, "l", "signed long"),
(11, "f", "float"),
(12, "d", "double")]))
2015-09-22 13:31:59 +03:00
@_register_loader(1, 1) # Basic type, except for the legacy API.
def load_byte(self, data, legacy_api=True):
return data
2015-09-22 13:31:59 +03:00
@_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"):
2010-07-31 06:52:47 +04:00
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
if sys.version_info[0] == 2:
2015-09-22 13:31:59 +03:00
value = value.decode('ascii', 'replace')
return b"" + value.encode('ascii', 'replace') + b"\0"
2010-07-31 06:52:47 +04:00
@_register_loader(5, 8)
def load_rational(self, data, legacy_api=True):
vals = self._unpack("{}L".format(len(data) // 4), data)
2017-04-20 14:14:23 +03:00
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]))
2010-07-31 06:52:47 +04:00
@_register_writer(5)
def write_rational(self, *values):
2014-12-30 13:57:45 +03:00
return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31))
for frac in values)
@_register_loader(7, 1)
def load_undefined(self, data, legacy_api=True):
2010-07-31 06:52:47 +04:00
return data
@_register_writer(7)
def write_undefined(self, value):
return value
2010-07-31 06:52:47 +04:00
@_register_loader(10, 8)
def load_signed_rational(self, data, legacy_api=True):
vals = self._unpack("{}l".format(len(data) // 4), data)
2017-04-20 14:14:23 +03:00
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]))
2010-07-31 06:52:47 +04:00
@_register_writer(10)
def write_signed_rational(self, *values):
2014-12-30 13:57:45 +03:00
return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30))
for frac in values)
2010-07-31 06:52:47 +04:00
2015-09-11 20:45:10 +03:00
def _ensure_read(self, fp, size):
ret = fp.read(size)
if len(ret) != size:
raise IOError("Corrupt EXIF data. " +
"Expecting to read %d bytes but only got %d. " %
(size, len(ret)))
return ret
def load(self, fp):
2010-07-31 06:52:47 +04:00
self.reset()
self._offset = fp.tell()
2010-07-31 06:52:47 +04:00
2015-09-11 20:45:10 +03:00
try:
2015-09-22 13:31:59 +03:00
for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]):
2017-05-27 23:55:14 +03:00
tag, typ, count, data = self._unpack("HHL4s",
self._ensure_read(fp, 12))
2015-09-10 18:33:14 +03:00
if DEBUG:
tagname = TiffTags.lookup(tag).name
2015-09-11 20:45:10 +03:00
typname = TYPES.get(typ, "unknown")
print("tag: %s (%d) - type: %s (%d)" %
(tagname, tag, typname, typ), end=" ")
try:
unit_size, handler = self._load_dispatch[typ]
except KeyError:
if DEBUG:
print("- unsupported type", typ)
continue # ignore unsupported type
size = count * unit_size
if size > 4:
here = fp.tell()
offset, = self._unpack("L", data)
if DEBUG:
print("Tag Location: %s - Data Location: %s" %
(here, offset), end=" ")
fp.seek(offset)
data = ImageFile._safe_read(fp, size)
fp.seek(here)
2010-07-31 06:52:47 +04:00
else:
2015-09-11 20:45:10 +03:00
data = data[:size]
2010-07-31 06:52:47 +04:00
2015-09-11 20:45:10 +03:00
if len(data) != size:
warnings.warn("Possibly corrupt EXIF data. "
2017-05-27 23:55:14 +03:00
"Expecting to read %d bytes but only got %d."
" Skipping tag %s" % (size, len(data), tag))
2015-09-11 20:45:10 +03:00
continue
2010-07-31 06:52:47 +04:00
if not data:
continue
2015-09-11 20:45:10 +03:00
self._tagdata[tag] = data
self.tagtype[tag] = typ
if DEBUG:
if size > 32:
print("- value: <table: %d bytes>" % size)
else:
print("- value:", self[tag])
2015-09-22 13:31:59 +03:00
self.next, = self._unpack("L", self._ensure_read(fp, 4))
2015-09-11 20:45:10 +03:00
except IOError as msg:
warnings.warn(str(msg))
return
2015-09-22 13:31:59 +03:00
2010-07-31 06:52:47 +04:00
def save(self, fp):
2014-12-30 13:57:45 +03:00
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))
# FIXME What about tagdata?
fp.write(self._pack("H", len(self._tags_v2)))
2010-07-31 06:52:47 +04:00
entries = []
offset = fp.tell() + len(self._tags_v2) * 12 + 4
2010-07-31 06:52:47 +04:00
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)
if DEBUG:
2015-08-25 15:27:18 +03:00
print("Tag %s, Type: %s, Value: %s" % (tag, typ, value))
values = value if isinstance(value, tuple) else (value,)
data = self._write_dispatch[typ](self, *values)
if DEBUG:
tagname = TiffTags.lookup(tag).name
typname = TYPES.get(typ, "unknown")
print("save: %s (%d) - type: %s (%d)" %
(tagname, tag, typname, typ), end=" ")
if len(data) >= 16:
print("- value: <table: %d bytes>" % len(data))
2010-07-31 06:52:47 +04:00
else:
print("- value:", values)
2010-07-31 06:52:47 +04:00
# count is sum of lengths for string and arbitrary data
count = len(data) if typ in [2, 7] else 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""))
2010-07-31 06:52:47 +04:00
else:
2017-05-27 23:55:14 +03:00
entries.append((tag, typ, count, self._pack("L", offset),
data))
2015-09-22 13:31:59 +03:00
offset += (len(data) + 1) // 2 * 2 # pad to word
2010-07-31 06:52:47 +04:00
# 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")
2014-12-30 13:57:45 +03:00
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:
if DEBUG > 1:
print(tag, typ, count, repr(value), repr(data))
2014-12-30 13:57:45 +03:00
fp.write(self._pack("HHL4s", tag, typ, count, value))
2010-07-31 06:52:47 +04:00
# -- overwrite here for multi-page --
fp.write(b"\0\0\0\0") # end of entries
2010-07-31 06:52:47 +04:00
# pass 3: write auxiliary data to file
for tag, typ, count, value, data in entries:
2010-07-31 06:52:47 +04:00
fp.write(data)
if len(data) & 1:
fp.write(b"\0")
2010-07-31 06:52:47 +04:00
return offset
2017-05-27 23:55:14 +03:00
2015-09-11 20:09:14 +03:00
ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items():
name = name.replace(" ", "_")
2015-09-11 20:09:14 +03:00
setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1])
setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx])
del _load_dispatch, _write_dispatch, idx, name
2015-09-22 13:31:59 +03:00
# Legacy ImageFileDirectory support.
2015-09-11 20:09:14 +03:00
class ImageFileDirectory_v1(ImageFileDirectory_v2):
"""This class represents the **legacy** interface to a TIFF tag directory.
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
Exposes a dictionary interface of the tags in the directory::
ifd = ImageFileDirectory_v1()
ifd[key] = 'Some Data'
ifd.tagtype[key] = 2
print(ifd[key])
2015-09-14 17:01:57 +03:00
('Some Data',)
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
Also contains a dictionary of tag types as read from the tiff image file,
2015-09-22 13:31:59 +03:00
`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.
2015-09-14 17:01:57 +03:00
Values are returned as a tuple.
2015-09-22 13:31:59 +03:00
2015-09-14 17:01:57 +03:00
.. deprecated:: 3.0.0
"""
2015-09-11 20:09:14 +03:00
def __init__(self, *args, **kwargs):
ImageFileDirectory_v2.__init__(self, *args, **kwargs)
2015-09-22 13:31:59 +03:00
self._legacy_api = True
2015-09-11 20:09:14 +03:00
tags = property(lambda self: self._tags_v1)
2015-09-11 20:09:14 +03:00
tagdata = property(lambda self: self._tagdata)
@classmethod
def from_v2(cls, original):
2015-09-14 17:01:57 +03:00
""" 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`
2015-09-14 17:01:57 +03:00
"""
2015-09-11 20:09:14 +03:00
ifd = cls(prefix=original.prefix)
ifd._tagdata = original._tagdata
ifd.tagtype = original.tagtype
2015-09-22 13:31:59 +03:00
ifd.next = original.next # an indicator for multipage tiffs
2015-09-11 20:09:14 +03:00
return ifd
def to_v2(self):
2015-09-14 17:01:57 +03:00
""" 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.
2015-09-14 17:01:57 +03:00
:returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
"""
2015-09-22 13:31:59 +03:00
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
2015-09-22 13:31:59 +03:00
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):
2015-09-22 13:31:59 +03:00
for legacy_api in (False, True):
self._setitem(tag, value, legacy_api)
2015-09-22 13:31:59 +03:00
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
2015-09-22 13:31:59 +03:00
2015-09-11 20:09:14 +03:00
# undone -- switch this pointer when IFD_LEGACY_API == False
ImageFileDirectory = ImageFileDirectory_v1
2014-07-28 20:00:06 +04:00
2015-09-22 13:31:59 +03:00
2010-07-31 06:52:47 +04:00
##
# Image plugin for TIFF files.
class TiffImageFile(ImageFile.ImageFile):
format = "TIFF"
format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False
2010-07-31 06:52:47 +04:00
def _open(self):
"Open the first image in a TIFF file"
# Header
ifh = self.fp.read(8)
# image file directory (tag dictionary)
2015-09-11 20:09:14 +03:00
self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy tag/ifd entries will be filled in later
self.tag = self.ifd = None
2010-07-31 06:52:47 +04:00
# setup frame pointers
2015-09-11 20:09:14 +03:00
self.__first = self.__next = self.tag_v2.next
2010-07-31 06:52:47 +04:00
self.__frame = -1
self.__fp = self.fp
self._frame_pos = []
self._n_frames = None
2015-06-30 06:25:00 +03:00
self._is_animated = None
2010-07-31 06:52:47 +04:00
if DEBUG:
2015-08-25 15:27:18 +03:00
print("*** TiffImageFile._open ***")
print("- __first:", self.__first)
print("- ifh: ", ifh)
2014-07-28 20:00:06 +04:00
# and load the first frame
2010-07-31 06:52:47 +04:00
self._seek(0)
@property
def n_frames(self):
if self._n_frames is None:
current = self.tell()
try:
while True:
self._seek(self.tell() + 1)
except EOFError:
self._n_frames = self.tell() + 1
self.seek(current)
return self._n_frames
2015-06-30 06:25:00 +03:00
@property
def is_animated(self):
if self._is_animated is None:
if self._n_frames is not None:
self._is_animated = self._n_frames != 1
else:
current = self.tell()
2015-06-30 06:25:00 +03:00
try:
self.seek(1)
self._is_animated = True
except EOFError:
self._is_animated = False
2015-06-30 06:25:00 +03:00
self.seek(current)
2015-06-30 06:25:00 +03:00
return self._is_animated
2010-07-31 06:52:47 +04:00
def seek(self, frame):
"Select a given frame as current image"
2015-04-23 11:06:32 +03:00
self._seek(max(frame, 0)) # Questionable backwards compatibility.
# 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)
2010-07-31 06:52:47 +04:00
def _seek(self, frame):
self.fp = self.__fp
while len(self._frame_pos) <= frame:
2010-07-31 06:52:47 +04:00
if not self.__next:
raise EOFError("no more images in TIFF file")
if DEBUG:
2015-08-28 11:56:48 +03:00
print("Seeking to frame %s, on frame %s, "
2015-08-25 15:27:18 +03:00
"__next %s, location: %s" %
(frame, self.__frame, self.__next, self.fp.tell()))
# reset python3 buffered io handle in case fp
# was passed to libtiff, invalidating the buffer
self.fp.tell()
2010-07-31 06:52:47 +04:00
self.fp.seek(self.__next)
self._frame_pos.append(self.__next)
if DEBUG:
2014-10-02 11:45:41 +04:00
print("Loading tags, location: %s" % self.fp.tell())
2015-09-11 20:09:14 +03:00
self.tag_v2.load(self.fp)
self.__next = self.tag_v2.next
self.__frame += 1
self.fp.seek(self._frame_pos[frame])
2015-09-11 20:09:14 +03:00
self.tag_v2.load(self.fp)
2017-01-02 02:17:39 +03:00
self.__next = self.tag_v2.next
2015-09-22 13:31:59 +03:00
# fill the legacy tag/ifd entries
2015-09-11 20:09:14 +03:00
self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
self.__frame = frame
2010-07-31 06:52:47 +04:00
self._setup()
def tell(self):
"Return the current frame number"
2010-07-31 06:52:47 +04:00
return self.__frame
def _decoder(self, rawmode, layer, tile=None):
2010-07-31 06:52:47 +04:00
"Setup decoder contexts"
args = None
if rawmode == "RGB" and self._planar_configuration == 2:
rawmode = rawmode[layer]
compression = self._compression
if compression == "raw":
args = (rawmode, 0, 1)
elif compression == "jpeg":
args = rawmode, ""
2015-09-11 20:09:14 +03:00
if JPEGTABLES in self.tag_v2:
2010-07-31 06:52:47 +04:00
# Hack to handle abbreviated JPEG headers
# Definition of JPEGTABLES is that the count
# is the number of bytes in the tables datastream
# so, it should always be 1 in our tag info
self.tile_prefix = self.tag_v2[JPEGTABLES]
2010-07-31 06:52:47 +04:00
elif compression == "packbits":
args = rawmode
elif compression == "tiff_lzw":
args = rawmode
2015-09-11 20:09:14 +03:00
if PREDICTOR in self.tag_v2:
2010-07-31 06:52:47 +04:00
# Section 14: Differencing Predictor
2015-09-11 20:09:14 +03:00
self.decoderconfig = (self.tag_v2[PREDICTOR],)
2010-07-31 06:52:47 +04:00
return args
def load(self):
if self.use_load_libtiff:
return self._load_libtiff()
return super(TiffImageFile, self).load()
2017-01-02 02:17:39 +03:00
def load_end(self):
# allow closing if we're on the first frame, there's no next
# This is the ImageFile.load path only, libtiff specific below.
if self.__frame == 0 and not self.__next:
self._close_exclusive_fp_after_loading = True
2017-01-02 02:17:39 +03:00
def _load_libtiff(self):
2013-11-22 08:57:48 +04:00
""" Overload method triggered when we detect a compressed tiff
Calls out to libtiff """
pixel = Image.Image.load(self)
if self.tile is None:
raise IOError("cannot load this image")
if not self.tile:
return pixel
self.load_prepare()
if not len(self.tile) == 1:
raise IOError("Not exactly one tile")
2014-07-28 20:00:06 +04:00
# (self._compression, (extents tuple),
# 0, (rawmode, self._compression, fp))
2015-04-08 14:12:37 +03:00
extents = self.tile[0][1]
args = list(self.tile[0][3]) + [self.tag_v2.offset]
# 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 for py3
# in _seek
if hasattr(self.fp, "flush"):
self.fp.flush()
except IOError:
# io.BytesIO have a fileno, but returns an IOError 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:
raise IOError("Couldn't set the image")
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.
2014-07-28 20:00:06 +04:00
# 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
2014-07-28 20:00:06 +04:00
# that returns an IOError if there's no underlying fp. Easier to
2015-05-29 07:59:54 +03:00
# deal with here by reordering.
if DEBUG:
2015-08-25 15:27:18 +03:00
print("have getvalue. just sending in a string from getvalue")
2014-07-28 20:00:06 +04:00
n, err = decoder.decode(self.fp.getvalue())
elif hasattr(self.fp, "fileno"):
# we've got a actual file on disk, pass in the fp.
if DEBUG:
2015-08-25 15:27:18 +03:00
print("have fileno, calling fileno version of the decoder.")
self.fp.seek(0)
2014-07-28 20:00:06 +04:00
# 4 bytes, otherwise the trace might error out
n, err = decoder.decode(b"fpfp")
else:
# we have something else.
if DEBUG:
2015-08-25 15:27:18 +03:00
print("don't have fileno or getvalue. just reading")
# UNDONE -- so much for that buffer size thing.
2014-07-28 20:00:06 +04:00
n, err = decoder.decode(self.fp.read())
self.tile = []
self.readonly = 0
2014-03-28 09:18:40 +04:00
# libtiff closed the fp in a, we need to close self.fp, if possible
if self._exclusive_fp:
2017-01-02 02:17:39 +03:00
if self.__frame == 0 and not self.__next:
2014-08-21 08:44:28 +04:00
self.fp.close()
self.fp = None # might be shared
if err < 0:
raise IOError(err)
return Image.Image.load(self)
2010-07-31 06:52:47 +04:00
def _setup(self):
"Setup this image object based on current tags"
2015-09-11 20:09:14 +03:00
if 0xBC01 in self.tag_v2:
raise IOError("Windows Media Photo files not yet supported")
2010-07-31 06:52:47 +04:00
# extract relevant tags
2015-09-11 20:09:14 +03:00
self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)
2010-07-31 06:52:47 +04:00
# photometric is a required tag, but not everyone is reading
# the specification
2015-09-11 20:09:14 +03:00
photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)
2010-07-31 06:52:47 +04:00
2015-09-11 20:09:14 +03:00
fillorder = self.tag_v2.get(FILLORDER, 1)
2010-07-31 06:52:47 +04:00
if DEBUG:
print("*** Summary ***")
print("- compression:", self._compression)
print("- photometric_interpretation:", photo)
print("- planar_configuration:", self._planar_configuration)
print("- fill_order:", fillorder)
2010-07-31 06:52:47 +04:00
# size
2015-09-11 20:09:14 +03:00
xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = self.tag_v2.get(IMAGELENGTH)
2010-07-31 06:52:47 +04:00
self.size = xsize, ysize
if DEBUG:
print("- size:", self.size)
2010-07-31 06:52:47 +04:00
sampleFormat = self.tag_v2.get(SAMPLEFORMAT, (1,))
if (len(sampleFormat) > 1
2017-04-20 14:14:23 +03:00
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,)
2010-07-31 06:52:47 +04:00
# mode: check photometric interpretation and bits per pixel
key = (
self.tag_v2.prefix, photo, sampleFormat, fillorder,
2015-09-11 20:09:14 +03:00
self.tag_v2.get(BITSPERSAMPLE, (1,)),
self.tag_v2.get(EXTRASAMPLES, ())
2010-07-31 06:52:47 +04:00
)
if DEBUG:
print("format key:", key)
2010-07-31 06:52:47 +04:00
try:
self.mode, rawmode = OPEN_INFO[key]
except KeyError:
if DEBUG:
print("- unsupported format")
raise SyntaxError("unknown pixel mode")
2010-07-31 06:52:47 +04:00
if DEBUG:
print("- raw mode:", rawmode)
print("- pil mode:", self.mode)
2010-07-31 06:52:47 +04:00
self.info["compression"] = self._compression
2016-02-05 01:57:13 +03:00
xres = self.tag_v2.get(X_RESOLUTION, 1)
yres = self.tag_v2.get(Y_RESOLUTION, 1)
2010-07-31 06:52:47 +04:00
if xres and yres:
resunit = self.tag_v2.get(RESOLUTION_UNIT)
2014-07-28 20:00:06 +04:00
if resunit == 2: # dots per inch
2010-07-31 06:52:47 +04:00
self.info["dpi"] = xres, yres
2014-07-28 20:00:06 +04:00
elif resunit == 3: # dots per centimeter. convert to dpi
2010-07-31 06:52:47 +04:00
self.info["dpi"] = xres * 2.54, yres * 2.54
2017-04-20 14:14:23 +03:00
elif resunit is None: # used to default to 1, but now 2)
self.info["dpi"] = xres, yres
2017-05-27 23:55:14 +03:00
# For backward compatibility,
# we also preserve the old behavior
self.info["resolution"] = xres, yres
2014-07-28 20:00:06 +04:00
else: # No absolute unit of measurement
2010-07-31 06:52:47 +04:00
self.info["resolution"] = xres, yres
# build tile descriptors
x = y = l = 0
self.tile = []
self.use_load_libtiff = False
2015-09-11 20:09:14 +03:00
if STRIPOFFSETS in self.tag_v2:
2010-07-31 06:52:47 +04:00
# striped image
2015-09-11 20:09:14 +03:00
offsets = self.tag_v2[STRIPOFFSETS]
h = self.tag_v2.get(ROWSPERSTRIP, ysize)
2010-07-31 06:52:47 +04:00
w = self.size[0]
if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3",
"group4", "tiff_jpeg",
"tiff_adobe_deflate",
"tiff_thunderscan",
"tiff_deflate",
"tiff_sgilog",
"tiff_sgilog24",
"tiff_raw_16"]:
# if DEBUG:
# print("Activating g4 compression for whole file")
# 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.
self.use_load_libtiff = True
# 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:
key = (
self.tag_v2.prefix, photo, sampleFormat, 1,
2015-09-11 20:09:14 +03:00
self.tag_v2.get(BITSPERSAMPLE, (1,)),
self.tag_v2.get(EXTRASAMPLES, ())
)
if DEBUG:
print("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 self.mode in ('I;16B', 'I;16') and 'I;16' in rawmode:
rawmode = 'I;16N'
# 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)
2010-07-31 06:52:47 +04:00
self.tile.append(
(self._compression,
(0, 0, w, ysize),
0, a))
a = None
else:
2016-11-06 04:38:59 +03:00
for i, offset in enumerate(offsets):
a = self._decoder(rawmode, l, i)
self.tile.append(
(self._compression,
2014-07-28 20:00:06 +04:00
(0, min(y, ysize), w, min(y+h, ysize)),
2016-11-06 04:38:59 +03:00
offset, a))
if DEBUG:
2015-08-25 15:27:18 +03:00
print("tiles: ", self.tile)
y = y + h
if y >= self.size[1]:
x = y = 0
l += 1
2010-07-31 06:52:47 +04:00
a = None
2015-09-11 20:09:14 +03:00
elif TILEOFFSETS in self.tag_v2:
2010-07-31 06:52:47 +04:00
# tiled image
2015-09-11 20:09:14 +03:00
w = self.tag_v2.get(322)
h = self.tag_v2.get(323)
2010-07-31 06:52:47 +04:00
a = None
2015-09-11 20:09:14 +03:00
for o in self.tag_v2[TILEOFFSETS]:
2010-07-31 06:52:47 +04:00
if not a:
a = self._decoder(rawmode, l)
# FIXME: this doesn't work if the image size
# is not a multiple of the tile size...
self.tile.append(
(self._compression,
2014-07-28 20:00:06 +04:00
(x, y, x+w, y+h),
o, a))
2010-07-31 06:52:47 +04:00
x = x + w
if x >= self.size[0]:
x, y = 0, y + h
if y >= self.size[1]:
x = y = 0
l += 1
2010-07-31 06:52:47 +04:00
a = None
else:
if DEBUG:
print("- unsupported data organization")
2010-07-31 06:52:47 +04:00
raise SyntaxError("unknown data organization")
# Fix up info.
if ICCPROFILE in self.tag_v2:
self.info['icc_profile'] = self.tag_v2[ICCPROFILE]
2010-07-31 06:52:47 +04:00
# fixup palette descriptor
if self.mode == "P":
2015-09-11 20:09:14 +03:00
palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
2017-05-27 23:55:14 +03:00
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
# Write TIFF files
2014-07-28 20:00:06 +04:00
# little endian is default except for image modes with
2015-05-29 07:59:54 +03:00
# explicit big endian byte-order
2010-07-31 06:52:47 +04:00
SAVE_INFO = {
2014-07-28 20:00:06 +04:00
# mode => rawmode, byteorder, photometrics,
# sampleformat, bitspersample, extra
2010-07-31 06:52:47 +04:00
"1": ("1", II, 1, 1, (1,), None),
"L": ("L", II, 1, 1, (8,), None),
2014-07-28 20:00:06 +04:00
"LA": ("LA", II, 1, 1, (8, 8), 2),
2010-07-31 06:52:47 +04:00
"P": ("P", II, 3, 1, (8,), None),
2014-07-28 20:00:06 +04:00
"PA": ("PA", II, 3, 1, (8, 8), 2),
2010-07-31 06:52:47 +04:00
"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),
2014-07-28 20:00:06 +04:00
"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),
2010-07-31 06:52:47 +04:00
"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),
}
2014-07-28 20:00:06 +04:00
2010-07-31 06:52:47 +04:00
def _save(im, fp, filename):
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError:
raise IOError("cannot write mode %s as TIFF" % im.mode)
2010-07-31 06:52:47 +04:00
2015-09-11 20:09:14 +03:00
ifd = ImageFileDirectory_v2(prefix=prefix)
2010-07-31 06:52:47 +04:00
2015-09-13 16:16:12 +03:00
compression = im.encoderinfo.get('compression',
im.info.get('compression', 'raw'))
2013-11-22 09:41:54 +04:00
libtiff = WRITE_LIBTIFF or compression != 'raw'
# required for color libtiff images
ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1)
2010-07-31 06:52:47 +04:00
ifd[IMAGEWIDTH] = im.size[0]
ifd[IMAGELENGTH] = im.size[1]
# write any arbitrary tags passed in as an ImageFileDirectory
2014-07-28 20:00:06 +04:00
info = im.encoderinfo.get("tiffinfo", {})
if DEBUG:
print("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:
2014-07-28 20:00:06 +04:00
pass # might not be an IFD, Might not have populated type
2010-07-31 06:52:47 +04:00
# additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
2015-09-13 12:53:47 +03:00
if hasattr(im, 'tag_v2'):
2010-07-31 06:52:47 +04:00
# preserve tags from original TIFF image file
for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION,
IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP):
2015-09-13 12:53:47 +03:00
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
if "icc_profile" in im.info:
ifd[ICCPROFILE] = im.info["icc_profile"]
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]
2010-07-31 06:52:47 +04:00
dpi = im.encoderinfo.get("dpi")
if dpi:
ifd[RESOLUTION_UNIT] = 2
ifd[X_RESOLUTION] = dpi[0]
ifd[Y_RESOLUTION] = dpi[1]
2010-07-31 06:52:47 +04:00
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 == "P":
lut = im.im.getpalette("RGB", "RGB;L")
ifd[COLORMAP] = tuple(i8(v) * 256 for v in lut)
2010-07-31 06:52:47 +04:00
# data orientation
stride = len(bits) * ((im.size[0]*bits[0]+7)//8)
2010-07-31 06:52:47 +04:00
ifd[ROWSPERSTRIP] = im.size[1]
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
2014-07-28 20:00:06 +04:00
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
# no compression by default:
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
2010-07-31 06:52:47 +04:00
if libtiff:
if DEBUG:
2015-08-25 15:27:18 +03:00
print("Saving using libtiff encoder")
print("Items: %s" % sorted(ifd.items()))
_fp = 0
if hasattr(fp, "fileno"):
2014-11-14 15:29:10 +03:00
try:
fp.seek(0)
_fp = os.dup(fp.fileno())
except io.UnsupportedOperation:
pass
# STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
# based on the data in the strip.
blocklist = [STRIPOFFSETS, STRIPBYTECOUNTS]
2014-07-28 20:00:06 +04:00
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(),
2016-02-05 01:57:13 +03:00
getattr(im, 'tag_v2', {}).items(),
legacy_ifd.items()):
# Libtiff can only process certain core items without adding
# them to the custom dictionary. It will segfault if it attempts
# to add a custom tag without the dictionary entry
#
# UNDONE -- add code for the custom dictionary
2016-02-05 01:57:13 +03:00
if tag not in TiffTags.LIBTIFF_CORE:
continue
if tag not in atts and tag not in blocklist:
if isinstance(value, unicode if bytes is str else str):
atts[tag] = value.encode('ascii', 'replace') + b"\0"
elif isinstance(value, IFDRational):
atts[tag] = float(value)
else:
atts[tag] = value
2013-10-21 20:30:06 +04:00
if DEBUG:
print("Converted items: %s" % sorted(atts.items()))
2013-11-22 08:57:48 +04:00
# 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'
a = (rawmode, compression, _fp, filename, atts)
2015-08-25 15:27:18 +03:00
# print(im.mode, compression, a, im.encoderconfig)
e = Image._getencoder(im.mode, 'libtiff', a, im.encoderconfig)
2014-07-28 20:00:06 +04:00
e.setimage(im.im, (0, 0)+im.size)
2014-01-08 06:21:24 +04:00
while True:
2014-07-28 20:00:06 +04:00
# 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 IOError("encoder error %d when writing image file" % s)
else:
offset = ifd.save(fp)
ImageFile._save(im, fp, [
2014-07-28 20:00:06 +04:00
("raw", (0, 0)+im.size, offset, (rawmode, stride, 1))
])
2010-07-31 06:52:47 +04:00
# -- helper for multi-page save --
if "_debug_multipage" in im.encoderinfo:
2014-07-28 20:00:06 +04:00
# just to access o32 and o16 (using correct byte order)
2010-07-31 06:52:47 +04:00
im._debug_multipage = ifd
2017-04-20 14:14:23 +03:00
2016-09-29 04:16:04 +03:00
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}
2016-09-29 04:16:04 +03:00
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 = io.open(fn, "w+b" if new else "r+b")
except IOError:
self.f = io.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):
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(bytearray(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("wrote only %u bytes but wanted 4" %
bytesWritten)
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("wrote only %u bytes but wanted 2" %
bytesWritten)
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("wrote only %u bytes but wanted 4" %
bytesWritten)
def writeShort(self, value):
bytesWritten = self.f.write(struct.pack(self.shortFmt, value))
if bytesWritten is not None and bytesWritten != 2:
raise RuntimeError("wrote only %u bytes but wanted 2" %
bytesWritten)
def writeLong(self, value):
bytesWritten = self.f.write(struct.pack(self.longFmt, value))
if bytesWritten is not None and bytesWritten != 4:
raise RuntimeError("wrote only %u bytes but wanted 4" %
bytesWritten)
def close(self):
self.finalize()
self.f.close()
def fixIFD(self):
numTags = self.readShort()
for i in range(numTags):
2017-05-27 23:55:14 +03:00
tag, fieldType, count = struct.unpack(self.tagFormat,
self.f.read(8))
2016-09-29 04:16:04 +03:00
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(4) # rewrite the type to LONG
self.f.seek(8, os.SEEK_CUR)
elif isShort:
self.rewriteLastShort(offset)
else:
self.rewriteLastLong(offset)
2017-04-20 14:14:23 +03:00
2016-09-29 04:16:04 +03:00
def _save_all(im, fp, filename):
encoderinfo = im.encoderinfo.copy()
encoderconfig = im.encoderconfig
append_images = encoderinfo.get("append_images", [])
if not hasattr(im, "n_frames") and not len(append_images):
2016-09-29 04:16:04 +03:00
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()
2016-09-29 04:16:04 +03:00
finally:
im.seek(cur_idx)
2017-05-27 23:55:14 +03:00
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
# Register
Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
Image.register_save(TiffImageFile.format, _save)
2016-09-29 04:16:04 +03:00
Image.register_save_all(TiffImageFile.format, _save_all)
2010-07-31 06:52:47 +04:00
2016-04-25 07:59:02 +03:00
Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])
2010-07-31 06:52:47 +04:00
Image.register_mime(TiffImageFile.format, "image/tiff")