diff options
Diffstat (limited to 'PIL/TiffImagePlugin.py')
-rw-r--r-- | PIL/TiffImagePlugin.py | 763 |
1 files changed, 763 insertions, 0 deletions
diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py new file mode 100644 index 0000000..178066c --- /dev/null +++ b/PIL/TiffImagePlugin.py @@ -0,0 +1,763 @@ +# +# The Python Imaging Library. +# $Id: TiffImagePlugin.py 2803 2006-07-31 19:18:57Z fredrik $ +# +# 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. +# + +__version__ = "1.3.5" + +import Image, ImageFile +import ImagePalette + +import array, string, sys + +try: + if sys.byteorder == "little": + byteorder = "II" + else: + byteorder = "MM" +except AttributeError: + if ord(array.array("i",[1]).tostring()[0]): + byteorder = "II" + else: + byteorder = "MM" + +# +# -------------------------------------------------------------------- +# Read TIFF files + +def il16(c,o=0): + return ord(c[o]) + (ord(c[o+1])<<8) +def il32(c,o=0): + return ord(c[o]) + (ord(c[o+1])<<8) + (ord(c[o+2])<<16) + (ord(c[o+3])<<24) +def ol16(i): + return chr(i&255) + chr(i>>8&255) +def ol32(i): + return chr(i&255) + chr(i>>8&255) + chr(i>>16&255) + chr(i>>24&255) + +def ib16(c,o=0): + return ord(c[o+1]) + (ord(c[o])<<8) +def ib32(c,o=0): + return ord(c[o+3]) + (ord(c[o+2])<<8) + (ord(c[o+1])<<16) + (ord(c[o])<<24) + +# 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 +EXTRASAMPLES = 338 +SAMPLEFORMAT = 339 +JPEGTABLES = 347 +COPYRIGHT = 33432 +IPTC_NAA_CHUNK = 33723 # newsphoto properties +PHOTOSHOP_CHUNK = 34377 # photoshop properties + +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 = { + # (PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, + # ExtraSamples) => mode, rawmode + (0, 1, 1, (1,), ()): ("1", "1;I"), + (0, 1, 2, (1,), ()): ("1", "1;IR"), + (0, 1, 1, (8,), ()): ("L", "L;I"), + (0, 1, 2, (8,), ()): ("L", "L;IR"), + (1, 1, 1, (1,), ()): ("1", "1"), + (1, 1, 2, (1,), ()): ("1", "1;R"), + (1, 1, 1, (8,), ()): ("L", "L"), + (1, 1, 1, (8,8), (2,)): ("LA", "LA"), + (1, 1, 2, (8,), ()): ("L", "L;R"), + (1, 1, 1, (16,), ()): ("I;16", "I;16"), + (1, 2, 1, (16,), ()): ("I;16S", "I;16S"), + (1, 2, 1, (32,), ()): ("I", "I;32S"), + (1, 3, 1, (32,), ()): ("F", "F;32F"), + (2, 1, 1, (8,8,8), ()): ("RGB", "RGB"), + (2, 1, 2, (8,8,8), ()): ("RGB", "RGB;R"), + (2, 1, 1, (8,8,8,8), (0,)): ("RGBX", "RGBX"), + (2, 1, 1, (8,8,8,8), (1,)): ("RGBA", "RGBa"), + (2, 1, 1, (8,8,8,8), (2,)): ("RGBA", "RGBA"), + (2, 1, 1, (8,8,8,8), (999,)): ("RGBA", "RGBA"), # corel draw 10 + (3, 1, 1, (1,), ()): ("P", "P;1"), + (3, 1, 2, (1,), ()): ("P", "P;1R"), + (3, 1, 1, (2,), ()): ("P", "P;2"), + (3, 1, 2, (2,), ()): ("P", "P;2R"), + (3, 1, 1, (4,), ()): ("P", "P;4"), + (3, 1, 2, (4,), ()): ("P", "P;4R"), + (3, 1, 1, (8,), ()): ("P", "P"), + (3, 1, 1, (8,8), (2,)): ("PA", "PA"), + (3, 1, 2, (8,), ()): ("P", "P;R"), + (5, 1, 1, (8,8,8,8), ()): ("CMYK", "CMYK"), + (6, 1, 1, (8,8,8), ()): ("YCbCr", "YCbCr"), + (8, 1, 1, (8,8,8), ()): ("LAB", "LAB"), +} + +PREFIXES = ["MM\000\052", "II\052\000", "II\xBC\000"] + +def _accept(prefix): + return prefix[:4] in PREFIXES + +## +# Wrapper for TIFF IFDs. + +class ImageFileDirectory: + + # represents a TIFF tag directory. to speed things up, + # we don't decode tags unless they're asked for. + + def __init__(self, prefix="II"): + self.prefix = prefix[:2] + if self.prefix == "MM": + self.i16, self.i32 = ib16, ib32 + # FIXME: save doesn't yet support big-endian mode... + 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.next = None + + # dictionary API (sort of) + + def keys(self): + return self.tagdata.keys() + self.tags.keys() + + def items(self): + items = self.tags.items() + for tag in self.tagdata.keys(): + items.append((tag, self[tag])) + return items + + 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 get(self, tag, default=None): + try: + return self[tag] + except KeyError: + return default + + 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 has_key(self, tag): + return self.tags.has_key(tag) or self.tagdata.has_key(tag) + + def __setitem__(self, tag, value): + if type(value) is not type(()): + value = (value,) + self.tags[tag] = value + + # load primitives + + load_dispatch = {} + + def load_byte(self, data): + l = [] + for i in range(len(data)): + l.append(ord(data[i])) + return tuple(l) + load_dispatch[1] = (1, load_byte) + + def load_string(self, data): + if data[-1:] == '\0': + data = data[:-1] + return data + 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 != byteorder: + a.byteswap() + return tuple(a) + load_dispatch[11] = (4, load_float) + + def load_double(self, data): + a = array.array("d", data) + if self.prefix != byteorder: + 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: + import TiffTags + tagname = TiffTags.TAGS.get(tag, "unknown") + typname = TiffTags.TYPES.get(typ, "unknown") + print "tag: %s (%d)" % (tagname, tag), + print "- type: %s (%d)" % (typname, typ), + + 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 + + if Image.DEBUG: + if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK): + 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 = self.tags.items() + tags.sort() + + 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: + + if Image.DEBUG: + import TiffTags + tagname = TiffTags.TAGS.get(tag, "unknown") + print "save: %s (%d)" % (tagname, tag), + print "- value:", value + + if type(value[0]) is type(""): + # string data + typ = 2 + data = value = string.join(value, "\0") + "\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 + else: + typ = 3 + for v in value: + if v >= 65536: + typ = 4 + if typ == 3: + data = string.join(map(o16, value), "") + else: + data = string.join(map(o32, value), "") + + # figure out if data fits into the directory + if len(data) == 4: + append((tag, typ, len(value), data, "")) + elif len(data) < 4: + append((tag, typ, len(value), data + (4-len(data))*"\0", "")) + 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) + fp.write("\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("\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 self.tag.has_key(JPEGTABLES): + # Hack to handle abbreviated JPEG headers + self.tile_prefix = self.tag[JPEGTABLES] + elif compression == "packbits": + args = rawmode + elif compression == "tiff_lzw": + args = rawmode + if self.tag.has_key(317): + # Section 14: Differencing Predictor + self.decoderconfig = (self.tag[PREDICTOR][0],) + + return args + + def _setup(self): + "Setup this image object based on current tags" + + if self.tag.has_key(0xBC01): + 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 = ( + 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 + + xdpi = getscalar(X_RESOLUTION, (1, 1)) + ydpi = getscalar(Y_RESOLUTION, (1, 1)) + + if xdpi and ydpi and getscalar(RESOLUTION_UNIT, 1) == 1: + xdpi = xdpi[0] / (xdpi[1] or 1) + ydpi = ydpi[0] / (ydpi[1] or 1) + self.info["dpi"] = xdpi, ydpi + + # build tile descriptors + x = y = l = 0 + self.tile = [] + if self.tag.has_key(STRIPOFFSETS): + # 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 self.tag.has_key(324): + # tiled image + w = getscalar(322) + h = getscalar(323) + a = None + for o in self.tag[324]: + 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 = map(lambda a: chr(a / 256), self.tag[COLORMAP]) + self.palette = ImagePalette.raw("RGB;L", string.join(palette, "")) + +# +# -------------------------------------------------------------------- +# Write TIFF files + +# little endian is default + +SAVE_INFO = { + # mode => rawmode, photometrics, sampleformat, bitspersample, extra + "1": ("1", 1, 1, (1,), None), + "L": ("L", 1, 1, (8,), None), + "LA": ("LA", 1, 1, (8,8), 2), + "P": ("P", 3, 1, (8,), None), + "PA": ("PA", 3, 1, (8,8), 2), + "I": ("I;32S", 1, 2, (32,), None), + "I;16": ("I;16", 1, 1, (16,), None), + "I;16S": ("I;16S", 1, 2, (16,), None), + "F": ("F;32F", 1, 3, (32,), None), + "RGB": ("RGB", 2, 1, (8,8,8), None), + "RGBX": ("RGBX", 2, 1, (8,8,8,8), 0), + "RGBA": ("RGBA", 2, 1, (8,8,8,8), 2), + "CMYK": ("CMYK", 5, 1, (8,8,8,8), None), + "YCbCr": ("YCbCr", 6, 1, (8,8,8), None), + "LAB": ("LAB", 8, 1, (8,8,8), None), +} + +def _cvt_res(value): + # convert value to TIFF rational number -- (numerator, denominator) + if type(value) in (type([]), type(())): + assert(len(value) % 2 == 0) + return value + if type(value) == type(1): + return (value, 1) + value = float(value) + return (int(value * 65536), 65536) + +def _save(im, fp, filename): + + try: + rawmode, photo, format, bits, extra = SAVE_INFO[im.mode] + except KeyError: + raise IOError, "cannot write mode %s as TIFF" % im.mode + + ifd = ImageFileDirectory() + + # tiff header (write via IFD to get everything right) + 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 im.tag.tagdata.has_key(key): + ifd[key] = im.tag.tagdata.get(key) + if im.encoderinfo.has_key("description"): + ifd[IMAGEDESCRIPTION] = im.encoderinfo["description"] + if im.encoderinfo.has_key("resolution"): + ifd[X_RESOLUTION] = ifd[Y_RESOLUTION] \ + = _cvt_res(im.encoderinfo["resolution"]) + if im.encoderinfo.has_key("x resolution"): + ifd[X_RESOLUTION] = _cvt_res(im.encoderinfo["x resolution"]) + if im.encoderinfo.has_key("y resolution"): + ifd[Y_RESOLUTION] = _cvt_res(im.encoderinfo["y resolution"]) + if im.encoderinfo.has_key("resolution unit"): + 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 im.encoderinfo.has_key("software"): + ifd[SOFTWARE] = im.encoderinfo["software"] + if im.encoderinfo.has_key("date time"): + ifd[DATE_TIME] = im.encoderinfo["date time"] + if im.encoderinfo.has_key("artist"): + ifd[ARTIST] = im.encoderinfo["artist"] + if im.encoderinfo.has_key("copyright"): + ifd[COPYRIGHT] = im.encoderinfo["copyright"] + + dpi = im.encoderinfo.get("dpi") + if dpi: + ifd[RESOLUTION_UNIT] = 1 + 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(map(lambda v: ord(v) * 256, 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)) + ]) + +# +# -------------------------------------------------------------------- +# 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") |