Pillow/PIL/TiffImagePlugin.py
Brian Crowell a7e3b2e47b py3k: The big push
There are two main issues fixed with this commit:

* bytes vs. str: All file, image, and palette data are now handled as
  bytes. A new _binary module consolidates the hacks needed to do this
  across Python versions. tostring/fromstring methods have been renamed to
  tobytes/frombytes, but the Python 2.6/2.7 versions alias them to the old
  names for compatibility. Users should move to tobytes/frombytes.

  One other potentially-breaking change is that text data in image files
  (such as tags, comments) are now explicitly handled with a specific
  character encoding in mind. This works well with the Unicode str in
  Python 3, but may trip up old code expecting a straight byte-for-byte
  translation to a Python string. This also required a change to Gohlke's
  tags tests (in Tests/test_file_png.py) to expect Unicode strings from
  the code.

* True div vs. floor div: Many division operations used the "/" operator
  to do floor division, which is now the "//" operator in Python 3. These
  were fixed.

As of this commit, on the first pass, I have one failing test (improper
handling of a slice object in a C module, test_imagepath.py) in Python 3,
and three that that I haven't tried running yet (test_imagegl,
test_imagegrab, and test_imageqt). I also haven't tested anything on
Windows. All but the three skipped tests run flawlessly against Pythons
2.6 and 2.7.
2013-01-10 08:46:56 -06:00

846 lines
27 KiB
Python

#
# 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 print_function
__version__ = "1.3.5"
from . import Image, ImageFile
from . import ImagePalette
from . import _binary
import array, sys
import collections
import itertools
II = b"II" # little-endian (intel-style)
MM = b"MM" # big-endian (motorola-style)
i8 = _binary.i8
o8 = _binary.o8
if sys.byteorder == "little":
native_prefix = II
else:
native_prefix = MM
#
# --------------------------------------------------------------------
# Read TIFF files
il16 = _binary.i16le
il32 = _binary.i32le
ol16 = _binary.o16le
ol32 = _binary.o32le
ib16 = _binary.i16be
ib32 = _binary.i32be
ob16 = _binary.o16be
ob32 = _binary.o32be
# 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
IPTC_NAA_CHUNK = 33723 # newsphoto properties
PHOTOSHOP_CHUNK = 34377 # photoshop properties
ICCPROFILE = 34675
EXIFIFD = 34665
XMP = 700
COMPRESSION_INFO = {
# Compression => pil compression name
1: "raw",
2: "tiff_ccitt",
3: "group3",
4: "group4",
5: "tiff_lzw",
6: "tiff_jpeg", # obsolete
7: "jpeg",
32771: "tiff_raw_16", # 16-bit padding
32773: "packbits"
}
OPEN_INFO = {
# (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
# ExtraSamples) => mode, rawmode
(II, 0, 1, 1, (1,), ()): ("1", "1;I"),
(II, 0, 1, 2, (1,), ()): ("1", "1;IR"),
(II, 0, 1, 1, (8,), ()): ("L", "L;I"),
(II, 0, 1, 2, (8,), ()): ("L", "L;IR"),
(II, 1, 1, 1, (1,), ()): ("1", "1"),
(II, 1, 1, 2, (1,), ()): ("1", "1;R"),
(II, 1, 1, 1, (8,), ()): ("L", "L"),
(II, 1, 1, 1, (8,8), (2,)): ("LA", "LA"),
(II, 1, 1, 2, (8,), ()): ("L", "L;R"),
(II, 1, 1, 1, (16,), ()): ("I;16", "I;16"),
(II, 1, 2, 1, (16,), ()): ("I;16S", "I;16S"),
(II, 1, 2, 1, (32,), ()): ("I", "I;32S"),
(II, 1, 3, 1, (32,), ()): ("F", "F;32F"),
(II, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
(II, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
(II, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
(II, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
(II, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"),
(II, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10
(II, 3, 1, 1, (1,), ()): ("P", "P;1"),
(II, 3, 1, 2, (1,), ()): ("P", "P;1R"),
(II, 3, 1, 1, (2,), ()): ("P", "P;2"),
(II, 3, 1, 2, (2,), ()): ("P", "P;2R"),
(II, 3, 1, 1, (4,), ()): ("P", "P;4"),
(II, 3, 1, 2, (4,), ()): ("P", "P;4R"),
(II, 3, 1, 1, (8,), ()): ("P", "P"),
(II, 3, 1, 1, (8,8), (2,)): ("PA", "PA"),
(II, 3, 1, 2, (8,), ()): ("P", "P;R"),
(II, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
(II, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
(II, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
(MM, 0, 1, 1, (1,), ()): ("1", "1;I"),
(MM, 0, 1, 2, (1,), ()): ("1", "1;IR"),
(MM, 0, 1, 1, (8,), ()): ("L", "L;I"),
(MM, 0, 1, 2, (8,), ()): ("L", "L;IR"),
(MM, 1, 1, 1, (1,), ()): ("1", "1"),
(MM, 1, 1, 2, (1,), ()): ("1", "1;R"),
(MM, 1, 1, 1, (8,), ()): ("L", "L"),
(MM, 1, 1, 1, (8,8), (2,)): ("LA", "LA"),
(MM, 1, 1, 2, (8,), ()): ("L", "L;R"),
(MM, 1, 1, 1, (16,), ()): ("I;16B", "I;16B"),
(MM, 1, 2, 1, (16,), ()): ("I;16BS", "I;16BS"),
(MM, 1, 2, 1, (32,), ()): ("I;32BS", "I;32BS"),
(MM, 1, 3, 1, (32,), ()): ("F;32BF", "F;32BF"),
(MM, 2, 1, 1, (8,8,8), ()): ("RGB", "RGB"),
(MM, 2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"),
(MM, 2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"),
(MM, 2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"),
(MM, 2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"),
(MM, 2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10
(MM, 3, 1, 1, (1,), ()): ("P", "P;1"),
(MM, 3, 1, 2, (1,), ()): ("P", "P;1R"),
(MM, 3, 1, 1, (2,), ()): ("P", "P;2"),
(MM, 3, 1, 2, (2,), ()): ("P", "P;2R"),
(MM, 3, 1, 1, (4,), ()): ("P", "P;4"),
(MM, 3, 1, 2, (4,), ()): ("P", "P;4R"),
(MM, 3, 1, 1, (8,), ()): ("P", "P"),
(MM, 3, 1, 1, (8,8), (2,)): ("PA", "PA"),
(MM, 3, 1, 2, (8,), ()): ("P", "P;R"),
(MM, 5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"),
(MM, 6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"),
(MM, 8, 1, 1, (8,8,8), ()): ("LAB", "LAB"),
}
PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"]
def _accept(prefix):
return prefix[:4] in PREFIXES
##
# Wrapper for TIFF IFDs.
class ImageFileDirectory(collections.MutableMapping):
# represents a TIFF tag directory. to speed things up,
# we don't decode tags unless they're asked for.
def __init__(self, prefix):
self.prefix = prefix[:2]
if self.prefix == MM:
self.i16, self.i32 = ib16, ib32
self.o16, self.o32 = ob16, ob32
elif self.prefix == II:
self.i16, self.i32 = il16, il32
self.o16, self.o32 = ol16, ol32
else:
raise SyntaxError("not a TIFF IFD")
self.reset()
def reset(self):
self.tags = {}
self.tagdata = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self.next = None
# dictionary API
def __len__(self):
return len(self.tagdata) + len(self.tags)
def __getitem__(self, tag):
try:
return self.tags[tag]
except KeyError:
type, data = self.tagdata[tag] # unpack on the fly
size, handler = self.load_dispatch[type]
self.tags[tag] = data = handler(self, data)
del self.tagdata[tag]
return data
def getscalar(self, tag, default=None):
try:
value = self[tag]
if len(value) != 1:
if tag == SAMPLEFORMAT:
# work around broken (?) matrox library
# (from Ted Wright, via Bob Klimek)
raise KeyError # use default
raise ValueError("not a scalar")
return value[0]
except KeyError:
if default is None:
raise
return default
def __contains__(self, tag):
return tag in self.tags or tag in self.tagdata
if bytes is str:
def has_key(self, tag):
return tag in self
def __setitem__(self, tag, value):
if not isinstance(value, tuple):
value = (value,)
self.tags[tag] = value
def __delitem__(self, tag):
self.tags.pop(tag, self.tagdata.pop(tag, None))
def __iter__(self):
return itertools.chain(self.tags.__iter__(), self.tagdata.__iter__())
# load primitives
load_dispatch = {}
def load_byte(self, data):
return data
load_dispatch[1] = (1, load_byte)
def load_string(self, data):
if data[-1:] == b'\0':
data = data[:-1]
return data.decode('latin-1', 'replace')
load_dispatch[2] = (1, load_string)
def load_short(self, data):
l = []
for i in range(0, len(data), 2):
l.append(self.i16(data, i))
return tuple(l)
load_dispatch[3] = (2, load_short)
def load_long(self, data):
l = []
for i in range(0, len(data), 4):
l.append(self.i32(data, i))
return tuple(l)
load_dispatch[4] = (4, load_long)
def load_rational(self, data):
l = []
for i in range(0, len(data), 8):
l.append((self.i32(data, i), self.i32(data, i+4)))
return tuple(l)
load_dispatch[5] = (8, load_rational)
def load_float(self, data):
a = array.array("f", data)
if self.prefix != native_prefix:
a.byteswap()
return tuple(a)
load_dispatch[11] = (4, load_float)
def load_double(self, data):
a = array.array("d", data)
if self.prefix != native_prefix:
a.byteswap()
return tuple(a)
load_dispatch[12] = (8, load_double)
def load_undefined(self, data):
# Untyped data
return data
load_dispatch[7] = (1, load_undefined)
def load(self, fp):
# load tag dictionary
self.reset()
i16 = self.i16
i32 = self.i32
for i in range(i16(fp.read(2))):
ifd = fp.read(12)
tag, typ = i16(ifd), i16(ifd, 2)
if Image.DEBUG:
from . import TiffTags
tagname = TiffTags.TAGS.get(tag, "unknown")
typname = TiffTags.TYPES.get(typ, "unknown")
print("tag: %s (%d)" % (tagname, tag), end=' ')
print("- type: %s (%d)" % (typname, typ), end=' ')
try:
dispatch = self.load_dispatch[typ]
except KeyError:
if Image.DEBUG:
print("- unsupported type", typ)
continue # ignore unsupported type
size, handler = dispatch
size = size * i32(ifd, 4)
# Get and expand tag value
if size > 4:
here = fp.tell()
fp.seek(i32(ifd, 8))
data = ImageFile._safe_read(fp, size)
fp.seek(here)
else:
data = ifd[8:8+size]
if len(data) != size:
raise IOError("not enough data")
self.tagdata[tag] = typ, data
self.tagtype[tag] = typ
if Image.DEBUG:
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP):
print("- value: <table: %d bytes>" % size)
else:
print("- value:", self[tag])
self.next = i32(fp.read(4))
# save primitives
def save(self, fp):
o16 = self.o16
o32 = self.o32
fp.write(o16(len(self.tags)))
# always write in ascending tag order
tags = sorted(self.tags.items())
directory = []
append = directory.append
offset = fp.tell() + len(self.tags) * 12 + 4
stripoffsets = None
# pass 1: convert tags to binary format
for tag, value in tags:
typ = None
if tag in self.tagtype:
typ = self.tagtype[tag]
if typ == 1:
# byte data
data = value
elif typ == 7:
# untyped data
data = value = b"".join(value)
elif isinstance(value[0], str):
# string data
typ = 2
data = value = b"\0".join(value.encode('ascii', 'replace')) + b"\0"
else:
# integer data
if tag == STRIPOFFSETS:
stripoffsets = len(directory)
typ = 4 # to avoid catch-22
elif tag in (X_RESOLUTION, Y_RESOLUTION):
# identify rational data fields
typ = 5
elif not typ:
typ = 3
for v in value:
if v >= 65536:
typ = 4
if typ == 3:
data = b"".join(map(o16, value))
else:
data = b"".join(map(o32, value))
if Image.DEBUG:
from . import TiffTags
tagname = TiffTags.TAGS.get(tag, "unknown")
typname = TiffTags.TYPES.get(typ, "unknown")
print("save: %s (%d)" % (tagname, tag), end=' ')
print("- type: %s (%d)" % (typname, typ), end=' ')
if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, ICCPROFILE, XMP):
size = len(data)
print("- value: <table: %d bytes>" % size)
else:
print("- value:", value)
# figure out if data fits into the directory
if len(data) == 4:
append((tag, typ, len(value), data, b""))
elif len(data) < 4:
append((tag, typ, len(value), data + (4-len(data))*b"\0", b""))
else:
count = len(value)
if typ == 5:
count = count // 2 # adjust for rational data field
append((tag, typ, count, o32(offset), data))
offset = offset + len(data)
if offset & 1:
offset = offset + 1 # word padding
# update strip offset data to point beyond auxiliary data
if stripoffsets is not None:
tag, typ, count, value, data = directory[stripoffsets]
assert not data, "multistrip support not yet implemented"
value = o32(self.i32(value) + offset)
directory[stripoffsets] = tag, typ, count, value, data
# pass 2: write directory to file
for tag, typ, count, value, data in directory:
if Image.DEBUG > 1:
print(tag, typ, count, repr(value), repr(data))
fp.write(o16(tag) + o16(typ) + o32(count) + value)
# -- overwrite here for multi-page --
fp.write(b"\0\0\0\0") # end of directory
# pass 3: write auxiliary data to file
for tag, typ, count, value, data in directory:
fp.write(data)
if len(data) & 1:
fp.write(b"\0")
return offset
##
# Image plugin for TIFF files.
class TiffImageFile(ImageFile.ImageFile):
format = "TIFF"
format_description = "Adobe TIFF"
def _open(self):
"Open the first image in a TIFF file"
# Header
ifh = self.fp.read(8)
if ifh[:4] not in PREFIXES:
raise SyntaxError("not a TIFF file")
# image file directory (tag dictionary)
self.tag = self.ifd = ImageFileDirectory(ifh[:2])
# setup frame pointers
self.__first = self.__next = self.ifd.i32(ifh, 4)
self.__frame = -1
self.__fp = self.fp
# and load the first frame
self._seek(0)
def seek(self, frame):
"Select a given frame as current image"
if frame < 0:
frame = 0
self._seek(frame)
def tell(self):
"Return the current frame number"
return self._tell()
def _seek(self, frame):
self.fp = self.__fp
if frame < self.__frame:
# rewind file
self.__frame = -1
self.__next = self.__first
while self.__frame < frame:
if not self.__next:
raise EOFError("no more images in TIFF file")
self.fp.seek(self.__next)
self.tag.load(self.fp)
self.__next = self.tag.next
self.__frame = self.__frame + 1
self._setup()
def _tell(self):
return self.__frame
def _decoder(self, rawmode, layer):
"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, ""
if JPEGTABLES in self.tag:
# Hack to handle abbreviated JPEG headers
self.tile_prefix = self.tag[JPEGTABLES]
elif compression == "packbits":
args = rawmode
elif compression == "tiff_lzw":
args = rawmode
if 317 in self.tag:
# Section 14: Differencing Predictor
self.decoderconfig = (self.tag[PREDICTOR][0],)
if ICCPROFILE in self.tag:
self.info['icc_profile'] = self.tag[ICCPROFILE]
return args
def _setup(self):
"Setup this image object based on current tags"
if 0xBC01 in self.tag:
raise IOError("Windows Media Photo files not yet supported")
getscalar = self.tag.getscalar
# extract relevant tags
self._compression = COMPRESSION_INFO[getscalar(COMPRESSION, 1)]
self._planar_configuration = getscalar(PLANAR_CONFIGURATION, 1)
# photometric is a required tag, but not everyone is reading
# the specification
photo = getscalar(PHOTOMETRIC_INTERPRETATION, 0)
fillorder = getscalar(FILLORDER, 1)
if Image.DEBUG:
print("*** Summary ***")
print("- compression:", self._compression)
print("- photometric_interpretation:", photo)
print("- planar_configuration:", self._planar_configuration)
print("- fill_order:", fillorder)
# size
xsize = getscalar(IMAGEWIDTH)
ysize = getscalar(IMAGELENGTH)
self.size = xsize, ysize
if Image.DEBUG:
print("- size:", self.size)
format = getscalar(SAMPLEFORMAT, 1)
# mode: check photometric interpretation and bits per pixel
key = (
self.tag.prefix, photo, format, fillorder,
self.tag.get(BITSPERSAMPLE, (1,)),
self.tag.get(EXTRASAMPLES, ())
)
if Image.DEBUG:
print("format key:", key)
try:
self.mode, rawmode = OPEN_INFO[key]
except KeyError:
if Image.DEBUG:
print("- unsupported format")
raise SyntaxError("unknown pixel mode")
if Image.DEBUG:
print("- raw mode:", rawmode)
print("- pil mode:", self.mode)
self.info["compression"] = self._compression
xres = getscalar(X_RESOLUTION, (1, 1))
yres = getscalar(Y_RESOLUTION, (1, 1))
if xres and yres:
xres = xres[0] / (xres[1] or 1)
yres = yres[0] / (yres[1] or 1)
resunit = getscalar(RESOLUTION_UNIT, 1)
if resunit == 2: # dots per inch
self.info["dpi"] = xres, yres
elif resunit == 3: # dots per centimeter. convert to dpi
self.info["dpi"] = xres * 2.54, yres * 2.54
else: # No absolute unit of measurement
self.info["resolution"] = xres, yres
# build tile descriptors
x = y = l = 0
self.tile = []
if STRIPOFFSETS in self.tag:
# striped image
h = getscalar(ROWSPERSTRIP, ysize)
w = self.size[0]
a = None
for o in self.tag[STRIPOFFSETS]:
if not a:
a = self._decoder(rawmode, l)
self.tile.append(
(self._compression,
(0, min(y, ysize), w, min(y+h, ysize)),
o, a))
y = y + h
if y >= self.size[1]:
x = y = 0
l = l + 1
a = None
elif TILEOFFSETS in self.tag:
# tiled image
w = getscalar(322)
h = getscalar(323)
a = None
for o in self.tag[TILEOFFSETS]:
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,
(x, y, x+w, y+h),
o, a))
x = x + w
if x >= self.size[0]:
x, y = 0, y + h
if y >= self.size[1]:
x = y = 0
l = l + 1
a = None
else:
if Image.DEBUG:
print("- unsupported data organization")
raise SyntaxError("unknown data organization")
# fixup palette descriptor
if self.mode == "P":
palette = [o8(a // 256) for a in self.tag[COLORMAP]]
self.palette = ImagePalette.raw("RGB;L", b"".join(palette))
#
# --------------------------------------------------------------------
# Write TIFF files
# little endian is default except for image modes with explict 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 _cvt_res(value):
# convert value to TIFF rational number -- (numerator, denominator)
if isinstance(value, collections.Sequence):
assert(len(value) % 2 == 0)
return value
if isinstance(value, int):
return (value, 1)
value = float(value)
return (int(value * 65536), 65536)
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)
ifd = ImageFileDirectory(prefix)
# -- multi-page -- skip TIFF header on subsequent pages
if fp.tell() == 0:
# tiff header (write via IFD to get everything right)
# PIL always starts the first IFD at offset 8
fp.write(ifd.prefix + ifd.o16(42) + ifd.o32(8))
ifd[IMAGEWIDTH] = im.size[0]
ifd[IMAGELENGTH] = im.size[1]
# additions written by Greg Couch, gregc@cgl.ucsf.edu
# inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
if hasattr(im, 'tag'):
# preserve tags from original TIFF image file
for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION):
if key in im.tag.tagdata:
ifd[key] = im.tag.tagdata.get(key)
# preserve some more tags from original TIFF image file
# -- 2008-06-06 Florian Hoech
ifd.tagtype = im.tag.tagtype
for key in (IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP):
if key in im.tag:
ifd[key] = im.tag[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"]
if "description" in im.encoderinfo:
ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"]
if "resolution" in im.encoderinfo:
ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \
= _cvt_res(im.encoderinfo["resolution"])
if "x resolution" in im.encoderinfo:
ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"])
if "y resolution" in im.encoderinfo:
ifd[Y_RESOLUTION] = _cvt_res(im.encoderinfo["y resolution"])
if "resolution unit" in im.encoderinfo:
unit = im.encoderinfo["resolution unit"]
if unit == "inch":
ifd[RESOLUTION_UNIT] = 2
elif unit == "cm" or unit == "centimeter":
ifd[RESOLUTION_UNIT] = 3
else:
ifd[RESOLUTION_UNIT] = 1
if "software" in im.encoderinfo:
ifd[SOFTWARE] = im.encoderinfo["software"]
if "date time" in im.encoderinfo:
ifd[DATE_TIME] = im.encoderinfo["date time"]
if "artist" in im.encoderinfo:
ifd[ARTIST] = im.encoderinfo["artist"]
if "copyright" in im.encoderinfo:
ifd[COPYRIGHT] = im.encoderinfo["copyright"]
dpi = im.encoderinfo.get("dpi")
if dpi:
ifd[RESOLUTION_UNIT] = 2
ifd[X_RESOLUTION] = _cvt_res(dpi[0])
ifd[Y_RESOLUTION] = _cvt_res(dpi[1])
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)
# data orientation
stride = len(bits) * ((im.size[0]*bits[0]+7)//8)
ifd[ROWSPERSTRIP] = im.size[1]
ifd[STRIPBYTECOUNTS] = stride * im.size[1]
ifd[STRIPOFFSETS] = 0 # this is adjusted by IFD writer
ifd[COMPRESSION] = 1 # no compression
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
#
# --------------------------------------------------------------------
# Register
Image.register_open("TIFF", TiffImageFile, _accept)
Image.register_save("TIFF", _save)
Image.register_extension("TIFF", ".tif")
Image.register_extension("TIFF", ".tiff")
Image.register_mime("TIFF", "image/tiff")