From 974bcc074be9eb791b9c66fe278f6638ad5e3e06 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 29 Dec 2014 16:48:01 +0100 Subject: [PATCH] Major rewrite of TIFF ImageFileDirectory. Do not represent scalar tags as 1-element tuples. Keep tag type and count information in TiffTags.TAGS. Normalize data in ImageFileDirectory.__setitem__: wrap and unwrap tuples as needed, convert rationals to floats. (To ensure consistency, make the "tags" attribute private.) Interpret byte data as a series of integers rather than a bytearray (which should only map to the "undefined" type). On Python3, if a str is assigned to an "undefined" tag, encode it as ASCII. Note that a large number of tags have been removed from TiffTags.TAGS because I do not have time to figure out the type and count of each of them. They should be restored before this gets merged in. This obviously breaks backwards compatibility in a lot of ways... --- PIL/TiffImagePlugin.py | 786 +++++++++++++------------------ PIL/TiffTags.py | 373 +++++---------- Tests/test_file_libtiff.py | 6 +- Tests/test_file_tiff.py | 48 +- Tests/test_file_tiff_metadata.py | 40 +- encode.c | 33 +- 6 files changed, 493 insertions(+), 793 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index f9f208af4..99f29865f 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -39,20 +39,23 @@ # See the README file for information on usage and redistribution. # -from __future__ import print_function +from __future__ import division, print_function from PIL import Image, ImageFile from PIL import ImagePalette from PIL import _binary -from PIL._util import isStringType -import warnings -import array -import sys import collections -import itertools -import os +from fractions import Fraction import io +import itertools +from numbers import Number +import os +import struct +import sys +import warnings + +from .TiffTags import TAGS, TYPES, TagInfo __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -67,25 +70,10 @@ 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 @@ -145,74 +133,74 @@ COMPRESSION_INFO_REV = dict([(v, k) for (k, v) in COMPRESSION_INFO.items()]) OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, # ExtraSamples) => mode, rawmode - (II, 0, 1, 1, (1,), ()): ("1", "1;I"), - (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, 0, 3, 1, (32,), ()): ("F", "F;32F"), - (II, 1, 1, 1, (1,), ()): ("1", "1"), - (II, 1, 1, 1, (4,), ()): ("L", "L;4"), - (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, (12,), ()): ("I;16", "I;12"), - (II, 1, 1, 1, (16,), ()): ("I;16", "I;16"), - (II, 1, 2, 1, (16,), ()): ("I;16S", "I;16S"), - (II, 1, 1, 1, (32,), ()): ("I", "I;32N"), - (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), ()): ("RGBA", "RGBA"), # missing ExtraSamples - (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"), + (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, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + (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, (12,), ()): ("I;16", "I;12"), + (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), + (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"), + (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), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (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", "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"), + (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", "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"), } @@ -223,248 +211,263 @@ def _accept(prefix): return prefix[:4] in PREFIXES +def _limit_rational(val, max_val): + inv = abs(val) > 1 + f = Fraction.from_float(1 / val if inv else val).limit_denominator(max_val) + n_d = (f.numerator, f.denominator) + return n_d[::-1] if inv else n_d + ## # Wrapper for TIFF IFDs. -class ImageFileDirectory(collections.MutableMapping): - """ This class represents a TIFF tag directory. To speed things - up, we don't decode tags unless they're asked for. +_load_dispatch = {} +_write_dispatch = {} + +class ImageFileDirectory(collections.MutableMapping): + """This class represents a TIFF tag directory. To speed things up, we + don't decode tags unless they're asked for. + + Exposes a dictionary interface of the tags in the directory - Exposes a dictionary interface of the tags in the directory ImageFileDirectory[key] = value value = ImageFileDirectory[key] - Also contains a dictionary of tag types as read from the tiff - image file, 'ImageFileDirectory.tagtype' + Also contains a dictionary of tag types as read from the tiff image file, + 'ImageFileDirectory.tagtype' - - Data Structures: + Data Structures: 'public' * self.tagtype = {} Key: numerical tiff tag number Value: integer corresponding to the data type from `TiffTags.TYPES` 'internal' - * self.tags = {} Key: numerical tiff tag number - Value: Decoded data, Generally a tuple. - * If set from __setval__ -- always a tuple - * Numeric types -- always a tuple - * String type -- not a tuple, returned as string - * Undefined data -- not a tuple, returned as bytes - * Byte -- not a tuple, returned as byte. - * self.tagdata = {} Key: numerical tiff tag number - Value: undecoded byte string from file + * self._tags = {} 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 - - Tags will be found in either self.tags or self.tagdata, but - not both. The union of the two should contain all the tags - from the Tiff image file. External classes shouldn't - reference these unless they're really sure what they're doing. - """ + Tags will be found in either self._tags or self._tagdata, but not + both. The union of the two should contain all the tags from the Tiff + image file. External classes shouldn't reference these unless they're + really sure what they're doing. + """ def __init__(self, prefix=II): """ - :prefix: 'II'|'MM' tiff endianness + :prefix: "II"|"MM" tiff endianness """ - 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 + self._prefix = prefix + if prefix == MM: + self._endian = ">" + elif prefix == II: + self._endian = "<" else: - raise SyntaxError("not a TIFF IFD") + raise ValueError("not a TIFF IFD") self.reset() + prefix = property(lambda self: self._prefix) + offset = property(lambda self: self._offset) + + @property + def offset(self): + return self._offset + def reset(self): - #: Tags is an incomplete dictionary of the tags of the image. - #: For a complete dictionary, use the as_dict method. - self.tags = {} - self.tagdata = {} + self._tags = {} + self._tagdata = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech - self.next = None - self.offset = None + self._next = None + self._offset = None def __str__(self): - return str(self.as_dict()) + return str(dict(self)) def as_dict(self): """Return a dictionary of the image's tags.""" - return dict(self.items()) + # FIXME Deprecate: use dict(self) + return dict(self) def named(self): """ Returns the complete tag dictionary, with named tags where possible. """ - from PIL import TiffTags - result = {} - for tag_code, value in self.items(): - tag_name = TiffTags.TAGS.get(tag_code, tag_code) - result[tag_name] = value - return result - - # dictionary API + return {TAGS.get(code, TagInfo()).name: value + for code, value in self.items()} def __len__(self): - return len(self.tagdata) + len(self.tags) + return len(self._tagdata) + len(self._tags) def __getitem__(self, tag): try: - return self.tags[tag] - except KeyError: - data = self.tagdata[tag] # unpack on the fly - type = self.tagtype[tag] - 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 + return self._tags[tag] + except KeyError: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data) # check type + del self._tagdata[tag] + return self[tag] def __contains__(self, tag): - return tag in self.tags or tag in self.tagdata + 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): - # tags are tuples for integers - # tags are not tuples for byte, string, and undefined data. - # see load_* - if not isinstance(value, tuple): - value = (value,) - self.tags[tag] = value + basetypes = (Number, bytes, str) + if bytes is str: + basetypes += unicode, + + info = TAGS.get(tag, TagInfo()) + values = [value] if isinstance(value, basetypes) else value + + if tag not in self.tagtype: + try: + self.tagtype[tag] = info.type + except KeyError: + self.tagtype[tag] = 7 + if 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: + values = [value.encode("ascii") if isinstance(value, str) else value + for value in values] + values = tuple(info.cvt_enum(value) for value in values) + if info.length == 1: + self._tags[tag], = values + else: + self._tags[tag] = values def __delitem__(self, tag): - self.tags.pop(tag, self.tagdata.pop(tag, None)) + self._tags.pop(tag, None) + self._tagdata.pop(tag, None) def __iter__(self): - return itertools.chain(self.tags.__iter__(), self.tagdata.__iter__()) + return itertools.chain(list(self._tags), list(self._tagdata)) - def items(self): - keys = list(self.__iter__()) - values = [self[key] for key in keys] - return zip(keys, values) + def unpack(self, fmt, data): + return struct.unpack(self._endian + fmt, data) - # load primitives + def pack(self, fmt, *values): + return struct.pack(self._endian + fmt, *values) - load_dispatch = {} + def _register_loader(idx, size): + def decorator(func): + from PIL.TiffTags import TYPES + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func + return func + return decorator - def load_byte(self, data): - return data - load_dispatch[1] = (1, load_byte) + def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func + return func + return decorator + def _register_basic(idx_fmt_name): + from PIL.TiffTags import TYPES + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = size, lambda self, data: ( + self.unpack("{}{}".format(len(data) // size, fmt), data)) + _write_dispatch[idx] = lambda self, *values: ( + b"".join(self.pack(fmt, value) for value in values)) + + list(map(_register_basic, + [(1, "B", "byte"), (3, "H", "short"), (4, "L", "long"), + (6, "b", "signed byte"), (8, "h", "signed short"), + (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) + + @_register_loader(2, 1) def load_string(self, data): - if data[-1:] == b'\0': + if data.endswith(b"\0"): data = data[:-1] - return data.decode('latin-1', 'replace') - load_dispatch[2] = (1, load_string) + return data.decode("latin-1", "replace") - 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) + @_register_writer(2) + def write_string(self, value): + # remerge of https://github.com/python-pillow/Pillow/pull/1416 + if sys.version_info[0] == 2: + value = value.decode('ascii', 'replace') + return b"" + value.encode('ascii', 'replace') + b"\0" + @_register_loader(5, 8) 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) + vals = self.unpack("{}L".format(len(data) // 4), data) + return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) - 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) + @_register_writer(5) + def write_rational(self, *values): + return b"".join(self.pack("2L", *_limit_rational(frac, 2 ** 31)) + for frac in values) + @_register_loader(7, 1) def load_undefined(self, data): - # Untyped data return data - load_dispatch[7] = (1, load_undefined) + + @_register_writer(7) + def write_undefined(self, value): + return value + + @_register_loader(10, 8) + def load_signed_rational(self, data): + vals = self.unpack("{}l".format(len(data) // 4), data) + return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) + + @_register_writer(10) + def write_signed_rational(self, *values): + return b"".join(self.pack("2L", *_limit_rational(frac, 2 ** 30)) + for frac in values) def load(self, fp): - # load tag dictionary self.reset() - self.offset = fp.tell() - - i16 = self.i16 - i32 = self.i32 - - for i in range(i16(fp.read(2))): - - ifd = fp.read(12) - if len(ifd) != 12: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read 12 bytes but only got %d." - % (len(ifd))) - continue - - tag, typ = i16(ifd), i16(ifd, 2) + self._offset = fp.tell() + for i in range(self.unpack("H", fp.read(2))[0]): + tag, typ, count, data = self.unpack("HHL4s", fp.read(12)) if DEBUG: - from PIL 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=' ') + tagname = TAGS.get(tag, TagInfo()).name + typname = TYPES.get(typ, "unknown") + print("tag: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") try: - dispatch = self.load_dispatch[typ] + unit_size, handler = self._load_dispatch[typ] except KeyError: if DEBUG: print("- unsupported type", typ) continue # ignore unsupported type - - size, handler = dispatch - - size = size * i32(ifd, 4) - - # Get and expand tag value + size = count * unit_size if size > 4: here = fp.tell() + offset, = self.unpack("L", data) if DEBUG: - print("Tag Location: %s" % here) - fp.seek(i32(ifd, 8)) - if DEBUG: - print("Data Location: %s" % fp.tell()) + print("Tag Location: %s - Data Location: %s" % + (here, offset), end=" ") + fp.seek(offset) data = ImageFile._safe_read(fp, size) fp.seek(here) else: - data = ifd[8:8+size] + data = data[:size] if len(data) != size: warnings.warn("Possibly corrupt EXIF data. " @@ -472,160 +475,89 @@ class ImageFileDirectory(collections.MutableMapping): "Skipping tag %s" % (size, len(data), tag)) continue - self.tagdata[tag] = data + self._tagdata[tag] = data self.tagtype[tag] = typ if DEBUG: - if tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, - ICCPROFILE, XMP): + if size > 32: print("- value: " % size) else: print("- value:", self[tag]) - ifd = fp.read(4) - if len(ifd) != 4: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read 4 bytes but only got %d." - % (len(ifd))) - return - - self.next = i32(ifd) - - # save primitives + self.next, = self.unpack("L", fp.read(4)) 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 + # FIXME What about tagdata? + fp.write(self.pack("H", len(self._tags))) + entries = [] + 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] - + # always write tags in ascending order + for tag, value in sorted(self._tags.items()): + if tag == STRIPOFFSETS: + stripoffsets = len(entries) + typ = self.tagtype.get(tag) if DEBUG: print("Tag %s, Type: %s, Value: %s" % (tag, typ, value)) - - if typ == 1: - # byte data - if isinstance(value, tuple): - data = value = value[-1] - else: - data = value - elif typ == 7: - # untyped data - data = value = b"".join(value) - elif typ in (11, 12): - # float value - tmap = {11: 'f', 12: 'd'} - if not isinstance(value, tuple): - value = (value,) - a = array.array(tmap[typ], value) - if self.prefix != native_prefix: - a.byteswap() - data = a.tostring() - elif isStringType(value[0]): - # string data - if isinstance(value, tuple): - value = value[-1] - typ = 2 - # was b'\0'.join(str), which led to \x00a\x00b sorts - # of strings which I don't see in in the wild tiffs - # and doesn't match the tiff spec: 8-bit byte that - # contains a 7-bit ASCII code; the last byte must be - # NUL (binary zero). Also, I don't think this was well - # exercised before. - if sys.version_info[0] == 2: - value = value.decode('ascii', 'replace') - data = value = b"" + 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) or typ == 5: - # identify rational data fields - typ = 5 - if isinstance(value[0], tuple): - # long name for flatten - value = tuple(itertools.chain.from_iterable(value)) - 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)) - + values = value if isinstance(value, tuple) else (value,) + data = self._write_dispatch[typ](self, *values) if DEBUG: - from PIL 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: " % size) + tagname = TAGS.get(tag, TagInfo()).name + typname = TYPES.get(typ, "unknown") + print("save: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") + if len(data) >= 16: + print("- value: " % len(data)) else: - print("- value:", value) + print("- value:", values) - # 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"")) + # 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"")) else: - count = len(value) - if typ == 5: - count = count // 2 # adjust for rational data field - - append((tag, typ, count, o32(offset), data)) - offset += len(data) - if offset & 1: - offset += 1 # word padding + entries.append((tag, typ, count, self.pack("L", offset), data)) + offset += (len(data) + 1) // 2 * 2 # pad to word # update strip offset data to point beyond auxiliary data if stripoffsets is not None: - tag, typ, count, value, data = directory[stripoffsets] - assert not data, "multistrip support not yet implemented" - value = o32(self.i32(value) + offset) - directory[stripoffsets] = tag, typ, count, value, data + tag, typ, count, value, data = entries[stripoffsets] + if data: + raise NotImplementedError( + "multistrip support not yet implemented") + value = self.pack("L", self.unpack("L", value)[0] + offset) + entries[stripoffsets] = tag, typ, count, value, data - # pass 2: write directory to file - for tag, typ, count, value, data in directory: + # 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)) - fp.write(o16(tag) + o16(typ) + o32(count) + value) + fp.write(self.pack("HHL4s", tag, typ, count, value)) # -- overwrite here for multi-page -- - fp.write(b"\0\0\0\0") # end of directory + fp.write(b"\0\0\0\0") # end of entries # pass 3: write auxiliary data to file - for tag, typ, count, value, data in directory: + for tag, typ, count, value, data in entries: fp.write(data) if len(data) & 1: fp.write(b"\0") return offset +ImageFileDirectory._load_dispatch = _load_dispatch +ImageFileDirectory._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + setattr(ImageFileDirectory, "load_" + name, _load_dispatch[idx][1]) + setattr(ImageFileDirectory, "write_" + name, _write_dispatch[idx]) +del _load_dispatch, _write_dispatch, idx, name + ## # Image plugin for TIFF files. @@ -648,7 +580,7 @@ class TiffImageFile(ImageFile.ImageFile): self.tag = self.ifd = ImageFileDirectory(ifh[:2]) # setup frame pointers - self.__first = self.__next = self.ifd.i32(ifh, 4) + self.__first, = self.__next, = self.ifd.unpack("L", ifh[4:]) self.__frame = -1 self.__fp = self.fp self._frame_pos = [] @@ -739,7 +671,8 @@ class TiffImageFile(ImageFile.ImageFile): args = rawmode, "" if JPEGTABLES in self.tag: # Hack to handle abbreviated JPEG headers - self.tile_prefix = self.tag[JPEGTABLES] + # FIXME This will fail with more than one value + self.tile_prefix, = self.tag[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": @@ -828,17 +761,15 @@ class TiffImageFile(ImageFile.ImageFile): 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) + self._compression = COMPRESSION_INFO[self.tag.get(COMPRESSION, 1)] + self._planar_configuration = self.tag.get(PLANAR_CONFIGURATION, 1) # photometric is a required tag, but not everyone is reading # the specification - photo = getscalar(PHOTOMETRIC_INTERPRETATION, 0) + photo = self.tag.get(PHOTOMETRIC_INTERPRETATION, 0) - fillorder = getscalar(FILLORDER, 1) + fillorder = self.tag.get(FILLORDER, 1) if DEBUG: print("*** Summary ***") @@ -848,14 +779,14 @@ class TiffImageFile(ImageFile.ImageFile): print("- fill_order:", fillorder) # size - xsize = getscalar(IMAGEWIDTH) - ysize = getscalar(IMAGELENGTH) + xsize = self.tag.get(IMAGEWIDTH) + ysize = self.tag.get(IMAGELENGTH) self.size = xsize, ysize if DEBUG: print("- size:", self.size) - format = getscalar(SAMPLEFORMAT, 1) + format = self.tag.get(SAMPLEFORMAT, (1,)) # mode: check photometric interpretation and bits per pixel key = ( @@ -878,8 +809,8 @@ class TiffImageFile(ImageFile.ImageFile): self.info["compression"] = self._compression - xres = getscalar(X_RESOLUTION, (1, 1)) - yres = getscalar(Y_RESOLUTION, (1, 1)) + xres = self.tag.get(X_RESOLUTION, (1, 1)) + yres = self.tag.get(Y_RESOLUTION, (1, 1)) if xres and not isinstance(xres, tuple): xres = (xres, 1.) @@ -888,7 +819,7 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: xres = xres[0] / (xres[1] or 1) yres = yres[0] / (yres[1] or 1) - resunit = getscalar(RESOLUTION_UNIT, 1) + resunit = self.tag.get(RESOLUTION_UNIT, 1) if resunit == 2: # dots per inch self.info["dpi"] = xres, yres elif resunit == 3: # dots per centimeter. convert to dpi @@ -902,7 +833,7 @@ class TiffImageFile(ImageFile.ImageFile): if STRIPOFFSETS in self.tag: # striped image offsets = self.tag[STRIPOFFSETS] - h = getscalar(ROWSPERSTRIP, ysize) + h = self.tag.get(ROWSPERSTRIP, ysize) w = self.size[0] if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4", "tiff_jpeg", @@ -991,8 +922,8 @@ class TiffImageFile(ImageFile.ImageFile): a = None elif TILEOFFSETS in self.tag: # tiled image - w = getscalar(322) - h = getscalar(323) + w = self.tag.get(322) + h = self.tag.get(323) a = None for o in self.tag[TILEOFFSETS]: if not a: @@ -1053,17 +984,6 @@ SAVE_INFO = { } -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: @@ -1085,7 +1005,7 @@ def _save(im, fp, filename): if not libtiff and 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)) + fp.write(ifd.prefix + ifd.pack("HL", 42, 8)) ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] @@ -1093,9 +1013,8 @@ def _save(im, fp, filename): # write any arbitrary tags passed in as an ImageFileDirectory info = im.encoderinfo.get("tiffinfo", {}) if DEBUG: - print("Tiffinfo Keys: %s" % info.keys) - keys = list(info.keys()) - for key in keys: + print("Tiffinfo Keys: %s" % list(info)) + for key in info: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] @@ -1117,31 +1036,29 @@ def _save(im, fp, filename): if "icc_profile" in im.info: ifd[ICCPROFILE] = im.info["icc_profile"] - for key, name, cvt in [ - (IMAGEDESCRIPTION, "description", lambda x: x), - (X_RESOLUTION, "resolution", _cvt_res), - (Y_RESOLUTION, "resolution", _cvt_res), - (X_RESOLUTION, "x_resolution", _cvt_res), - (Y_RESOLUTION, "y_resolution", _cvt_res), - (RESOLUTION_UNIT, "resolution_unit", - lambda x: {"inch": 2, "cm": 3, "centimeter": 3}.get(x, 1)), - (SOFTWARE, "software", lambda x: x), - (DATE_TIME, "date_time", lambda x: x), - (ARTIST, "artist", lambda x: x), - (COPYRIGHT, "copyright", lambda x: x)]: + 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")]: name_with_spaces = name.replace("_", " ") if "_" in name and name_with_spaces in im.encoderinfo: warnings.warn("%r is deprecated; use %r instead" % (name_with_spaces, name), DeprecationWarning) - ifd[key] = cvt(im.encoderinfo[name.replace("_", " ")]) + ifd[key] = im.encoderinfo[name.replace("_", " ")] if name in im.encoderinfo: - ifd[key] = cvt(im.encoderinfo[name]) + ifd[key] = im.encoderinfo[name] 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]) + ifd[X_RESOLUTION] = dpi[0] + ifd[Y_RESOLUTION] = dpi[1] if bits != (1,): ifd[BITSPERSAMPLE] = bits @@ -1169,7 +1086,7 @@ def _save(im, fp, filename): if libtiff: if DEBUG: print("Saving using libtiff encoder") - print(ifd.items()) + print("Items: %s" % sorted(ifd.items())) _fp = 0 if hasattr(fp, "fileno"): try: @@ -1183,52 +1100,19 @@ def _save(im, fp, filename): atts = {} # bits per sample is a single short in the tiff directory, not a list. atts[BITSPERSAMPLE] = bits[0] - if EXTRASAMPLES in ifd: - atts[EXTRASAMPLES] = list(ifd[EXTRASAMPLES]) # 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. for k, v in itertools.chain(ifd.items(), getattr(im, 'ifd', {}).items()): if k not in atts and k not in blocklist: - if type(v[0]) == tuple and len(v) > 1: - # A tuple of more than one rational tuples - # flatten to floats, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = [float(elt[0])/float(elt[1]) for elt in v] - continue - if type(v[0]) == tuple and len(v) == 1: - # A tuple of one rational tuples - # flatten to floats, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = float(v[0][0])/float(v[0][1]) - continue - if (type(v) == tuple and - (len(v) > 2 or - (len(v) == 2 and v[1] == 0))): - # List of ints? - # Avoid divide by zero in next if-clause - if type(v[0]) in (int, float): - atts[k] = list(v) - continue - if type(v) == tuple and len(v) == 2: - # one rational tuple - # flatten to float, - # following tiffcp.c->cpTag->TIFF_RATIONAL - atts[k] = float(v[0])/float(v[1]) - continue - if type(v) == tuple and len(v) == 1: - v = v[0] - # drop through - if isStringType(v): - atts[k] = bytes(v.encode('ascii', 'replace')) + b"\0" - continue + if isinstance(v, unicode if bytes is str else str): + atts[k] = v.encode('ascii', 'replace') + b"\0" else: - # int or similar atts[k] = v if DEBUG: - print(atts) + print("Converted items: %s" % sorted(atts.items())) # libtiff always expects the bytes in native order. # we're storing image byte order. So, if the rawmode diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index d15aa7ebe..422891aeb 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -17,291 +17,132 @@ # well-known TIFF tags. ## +from collections import namedtuple + +class TagInfo(namedtuple("_TagInfo", "value name type length enum")): + __slots__ = [] + + def __new__(cls, value=None, name="unknown", type=4, length=0, enum=None): + return super(TagInfo, cls).__new__( + cls, value, name, type, length, enum or {}) + + def cvt_enum(self, value): + return self.enum.get(value, value) + ## -# Map tag numbers (or tag number, tag value tuples) to tag names. +# Map tag numbers to tag info. TAGS = { - 254: "NewSubfileType", - 255: "SubfileType", - 256: "ImageWidth", - 257: "ImageLength", - 258: "BitsPerSample", + 254: ("NewSubfileType", 4, 1), + 255: ("SubfileType", 3, 1), + 256: ("ImageWidth", 4, 1), + 257: ("ImageLength", 4, 1), + 258: ("BitsPerSample", 3, 0), + 259: ("Compression", 3, 1, + {"Uncompressed": 1, "CCITT 1d": 2, "Group 3 Fax": 3, "Group 4 Fax": 4, + "LZW": 5, "JPEG": 6, "PackBits": 32773}), - 259: "Compression", - (259, 1): "Uncompressed", - (259, 2): "CCITT 1d", - (259, 3): "Group 3 Fax", - (259, 4): "Group 4 Fax", - (259, 5): "LZW", - (259, 6): "JPEG", - (259, 32773): "PackBits", + 262: ("PhotometricInterpretation", 3, 1, + {"WhiteIsZero": 0, "BlackIsZero": 1, "RGB": 2, "RBG Palette": 3, + "Transparency Mask": 4, "CMYK": 5, "YCbCr": 6, "CieLAB": 8, + "CFA": 32803, # TIFF/EP, Adobe DNG + "LinearRaw": 32892}), # Adobe DNG + 263: ("Thresholding", 3, 1), + 264: ("CellWidth", 3, 1), + 265: ("CellHeight", 3, 1), + 266: ("FillOrder", 3, 1), + 269: ("DocumentName", 2, 1), - 262: "PhotometricInterpretation", - (262, 0): "WhiteIsZero", - (262, 1): "BlackIsZero", - (262, 2): "RGB", - (262, 3): "RGB Palette", - (262, 4): "Transparency Mask", - (262, 5): "CMYK", - (262, 6): "YCbCr", - (262, 8): "CieLAB", - (262, 32803): "CFA", # TIFF/EP, Adobe DNG - (262, 32892): "LinearRaw", # Adobe DNG + 270: ("ImageDescription", 2, 1), + 271: ("Make", 2, 1), + 272: ("Model", 2, 1), + 273: ("StripOffsets", 4, 0), + 274: ("Orientation", 3, 1), + 277: ("SamplesPerPixel", 3, 1), + 278: ("RowsPerStrip", 4, 1), + 279: ("StripByteCounts", 4, 0), - 263: "Thresholding", - 264: "CellWidth", - 265: "CellHeight", - 266: "FillOrder", - 269: "DocumentName", + 280: ("MinSampleValue", 4, 0), + 281: ("MaxSampleValue", 3, 0), + 282: ("XResolution", 5, 1), + 283: ("YResolution", 5, 1), + 284: ("PlanarConfiguration", 3, 1, {"Contigous": 1, "Separate": 2}), + 285: ("PageName", 2, 1), + 286: ("XPosition", 5, 1), + 287: ("YPosition", 5, 1), + 288: ("FreeOffsets", 4, 1), + 289: ("FreeByteCounts", 4, 1), - 270: "ImageDescription", - 271: "Make", - 272: "Model", - 273: "StripOffsets", - 274: "Orientation", - 277: "SamplesPerPixel", - 278: "RowsPerStrip", - 279: "StripByteCounts", + 290: ("GrayResponseUnit", 3, 1), + 291: ("GrayResponseCurve", 3, 0), + 292: ("T4Options", 4, 1), + 293: ("T6Options", 4, 1), + 296: ("ResolutionUnit", 3, 1, {"inch": 1, "cm": 2}), + 297: ("PageNumber", 3, 2), - 280: "MinSampleValue", - 281: "MaxSampleValue", - 282: "XResolution", - 283: "YResolution", - 284: "PlanarConfiguration", - (284, 1): "Contigous", - (284, 2): "Separate", + 301: ("TransferFunction", 3, 0), + 305: ("Software", 2, 1), + 306: ("DateTime", 2, 1), - 285: "PageName", - 286: "XPosition", - 287: "YPosition", - 288: "FreeOffsets", - 289: "FreeByteCounts", + 315: ("Artist", 2, 1), + 316: ("HostComputer", 2, 1), + 317: ("Predictor", 3, 1), + 318: ("WhitePoint", 5, 2), + 319: ("PrimaryChromaticies", 3, 6), - 290: "GrayResponseUnit", - 291: "GrayResponseCurve", - 292: "T4Options", - 293: "T6Options", - 296: "ResolutionUnit", - 297: "PageNumber", + 320: ("ColorMap", 3, 0), + 321: ("HalftoneHints", 3, 2), + 322: ("TileWidth", 4, 1), + 323: ("TileLength", 4, 1), + 324: ("TileOffsets", 4, 0), + 325: ("TileByteCounts", 4, 0), - 301: "TransferFunction", - 305: "Software", - 306: "DateTime", + 332: ("InkSet", 3, 1), + 333: ("InkNames", 2, 1), + 334: ("NumberOfInks", 3, 1), + 336: ("DotRange", 3, 0), + 337: ("TargetPrinter", 2, 1), + 338: ("ExtraSamples", 1, 0), + 339: ("SampleFormat", 3, 0), - 315: "Artist", - 316: "HostComputer", - 317: "Predictor", - 318: "WhitePoint", - 319: "PrimaryChromaticies", - - 320: "ColorMap", - 321: "HalftoneHints", - 322: "TileWidth", - 323: "TileLength", - 324: "TileOffsets", - 325: "TileByteCounts", - - 332: "InkSet", - 333: "InkNames", - 334: "NumberOfInks", - 336: "DotRange", - 337: "TargetPrinter", - 338: "ExtraSamples", - 339: "SampleFormat", - - 340: "SMinSampleValue", - 341: "SMaxSampleValue", - 342: "TransferRange", - - 347: "JPEGTables", + 340: ("SMinSampleValue", 12, 0), + 341: ("SMaxSampleValue", 12, 0), + 342: ("TransferRange", 3, 6), # obsolete JPEG tags - 512: "JPEGProc", - 513: "JPEGInterchangeFormat", - 514: "JPEGInterchangeFormatLength", - 515: "JPEGRestartInterval", - 517: "JPEGLosslessPredictors", - 518: "JPEGPointTransforms", - 519: "JPEGQTables", - 520: "JPEGDCTables", - 521: "JPEGACTables", + 512: ("JPEGProc", 3, 1), + 513: ("JPEGInterchangeFormat", 4, 1), + 514: ("JPEGInterchangeFormatLength", 4, 1), + 515: ("JPEGRestartInterval", 3, 1), + 517: ("JPEGLosslessPredictors", 3, 0), + 518: ("JPEGPointTransforms", 3, 0), + 519: ("JPEGQTables", 4, 0), + 520: ("JPEGDCTables", 4, 0), + 521: ("JPEGACTables", 4, 0), - 529: "YCbCrCoefficients", - 530: "YCbCrSubSampling", - 531: "YCbCrPositioning", - 532: "ReferenceBlackWhite", + 529: ("YCbCrCoefficients", 5, 3), + 530: ("YCbCrSubSampling", 3, 2), + 531: ("YCbCrPositioning", 3, 1), + 532: ("ReferenceBlackWhite", 4, 0), - # XMP - 700: "XMP", + 33432: ("Copyright", 2, 1), - 33432: "Copyright", - - # various extensions (should check specs for "official" names) - 33723: "IptcNaaInfo", - 34377: "PhotoshopInfo", - - # Exif IFD - 34665: "ExifIFD", - - # ICC Profile - 34675: "ICCProfile", - - # Additional Exif Info - 33434: "ExposureTime", - 33437: "FNumber", - 34850: "ExposureProgram", - 34852: "SpectralSensitivity", - 34853: "GPSInfoIFD", - 34855: "ISOSpeedRatings", - 34856: "OECF", - 34864: "SensitivityType", - 34865: "StandardOutputSensitivity", - 34866: "RecommendedExposureIndex", - 34867: "ISOSpeed", - 34868: "ISOSpeedLatitudeyyy", - 34869: "ISOSpeedLatitudezzz", - 36864: "ExifVersion", - 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", - 37121: "ComponentsConfiguration", - 37122: "CompressedBitsPerPixel", - 37377: "ShutterSpeedValue", - 37378: "ApertureValue", - 37379: "BrightnessValue", - 37380: "ExposureBiasValue", - 37381: "MaxApertureValue", - 37382: "SubjectDistance", - 37383: "MeteringMode", - 37384: "LightSource", - 37385: "Flash", - 37386: "FocalLength", - 37396: "SubjectArea", - 37500: "MakerNote", - 37510: "UserComment", - 37520: "SubSec", - 37521: "SubSecTimeOriginal", - 37522: "SubsecTimeDigitized", - 40960: "FlashPixVersion", - 40961: "ColorSpace", - 40962: "PixelXDimension", - 40963: "PixelYDimension", - 40964: "RelatedSoundFile", - 40965: "InteroperabilityIFD", - 41483: "FlashEnergy", - 41484: "SpatialFrequencyResponse", - 41486: "FocalPlaneXResolution", - 41487: "FocalPlaneYResolution", - 41488: "FocalPlaneResolutionUnit", - 41492: "SubjectLocation", - 41493: "ExposureIndex", - 41495: "SensingMethod", - 41728: "FileSource", - 41729: "SceneType", - 41730: "CFAPattern", - 41985: "CustomRendered", - 41986: "ExposureMode", - 41987: "WhiteBalance", - 41988: "DigitalZoomRatio", - 41989: "FocalLengthIn35mmFilm", - 41990: "SceneCaptureType", - 41991: "GainControl", - 41992: "Contrast", - 41993: "Saturation", - 41994: "Sharpness", - 41995: "DeviceSettingDescription", - 41996: "SubjectDistanceRange", - 42016: "ImageUniqueID", - 42032: "CameraOwnerName", - 42033: "BodySerialNumber", - 42034: "LensSpecification", - 42035: "LensMake", - 42036: "LensModel", - 42037: "LensSerialNumber", - 42240: "Gamma", - - # MP Info - 45056: "MPFVersion", - 45057: "NumberOfImages", - 45058: "MPEntry", - 45059: "ImageUIDList", - 45060: "TotalFrames", - 45313: "MPIndividualNum", - 45569: "PanOrientation", - 45570: "PanOverlap_H", - 45571: "PanOverlap_V", - 45572: "BaseViewpointNum", - 45573: "ConvergenceAngle", - 45574: "BaselineLength", - 45575: "VerticalDivergence", - 45576: "AxisDistance_X", - 45577: "AxisDistance_Y", - 45578: "AxisDistance_Z", - 45579: "YawAngle", - 45580: "PitchAngle", - 45581: "RollAngle", - - # Adobe DNG - 50706: "DNGVersion", - 50707: "DNGBackwardVersion", - 50708: "UniqueCameraModel", - 50709: "LocalizedCameraModel", - 50710: "CFAPlaneColor", - 50711: "CFALayout", - 50712: "LinearizationTable", - 50713: "BlackLevelRepeatDim", - 50714: "BlackLevel", - 50715: "BlackLevelDeltaH", - 50716: "BlackLevelDeltaV", - 50717: "WhiteLevel", - 50718: "DefaultScale", - 50719: "DefaultCropOrigin", - 50720: "DefaultCropSize", - 50778: "CalibrationIlluminant1", - 50779: "CalibrationIlluminant2", - 50721: "ColorMatrix1", - 50722: "ColorMatrix2", - 50723: "CameraCalibration1", - 50724: "CameraCalibration2", - 50725: "ReductionMatrix1", - 50726: "ReductionMatrix2", - 50727: "AnalogBalance", - 50728: "AsShotNeutral", - 50729: "AsShotWhiteXY", - 50730: "BaselineExposure", - 50731: "BaselineNoise", - 50732: "BaselineSharpness", - 50733: "BayerGreenSplit", - 50734: "LinearResponseLimit", - 50735: "CameraSerialNumber", - 50736: "LensInfo", - 50737: "ChromaBlurRadius", - 50738: "AntiAliasStrength", - 50740: "DNGPrivateData", - 50741: "MakerNoteSafety", - 50780: "BestQualityScale", - - # ImageJ - 50838: "ImageJMetaDataByteCounts", # private tag registered with Adobe - 50839: "ImageJMetaData", # private tag registered with Adobe + # FIXME add more tags here + 50741: ("MakerNoteSafety", 3, 1, {0: "Unsafe", 1: "Safe"}), + 50780: ("BestQualityScale", 5, 1), + # private tags registered with Adobe + 50838: ("ImageJMetaDataByteCounts", 4, 1), + 50839: ("ImageJMetaData", 7, 1) } + +for k, v in TAGS.items(): + TAGS[k] = TagInfo(k, *v) +del k, v + + ## -# Map type numbers to type names. +# Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = { - - 1: "byte", - 2: "ascii", - 3: "short", - 4: "long", - 5: "rational", - 6: "signed byte", - 7: "undefined", - 8: "signed short", - 9: "signed long", - 10: "signed rational", - 11: "float", - 12: "double", - -} +TYPES = {} diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 8d5b383a9..703ba3015 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -142,9 +142,8 @@ class TestFileLibTiff(LibTiffTestCase): for tag, value in reloaded.items(): if tag not in ignored: if tag.endswith('Resolution'): - val = original[tag] self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], + original[tag], value, msg="%s didn't roundtrip" % tag) else: self.assertEqual( @@ -153,9 +152,8 @@ class TestFileLibTiff(LibTiffTestCase): for tag, value in original.items(): if tag not in ignored: if tag.endswith('Resolution'): - val = reloaded[tag] self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], + original[tag], value, msg="%s didn't roundtrip" % tag) else: self.assertEqual( diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 7d0871026..147507f54 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -74,11 +74,9 @@ class TestFileTiff(PillowTestCase): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" im = Image.open(filename) - assert isinstance(im.tag.tags[X_RESOLUTION][0], tuple) - assert isinstance(im.tag.tags[Y_RESOLUTION][0], tuple) # Try to read a file where X,Y_RESOLUTION are ints - im.tag.tags[X_RESOLUTION] = (72,) - im.tag.tags[Y_RESOLUTION] = (72,) + im.tag[X_RESOLUTION] = (72,) + im.tag[Y_RESOLUTION] = (72,) im._setup() self.assertEqual(im.info['dpi'], (72., 72.)) @@ -228,10 +226,9 @@ class TestFileTiff(PillowTestCase): self.assertIsInstance(ret, dict) self.assertEqual( - ret, {256: (55,), 257: (43,), 258: (8, 8, 8, 8), 259: (1,), - 262: (2,), 296: (2,), 273: (8,), 338: (1,), 277: (4,), - 279: (9460,), 282: ((720000, 10000),), - 283: ((720000, 10000),), 284: (1,)}) + ret, {256: 55, 257: 43, 258: (8, 8, 8, 8), 259: 1, 262: 2, 296: 2, + 273: (8,), 338: (1,), 277: 4, 279: (9460,), + 282: 72.0, 283: 72.0, 284: 1}) def test__delitem__(self): # Arrange @@ -255,7 +252,7 @@ class TestFileTiff(PillowTestCase): ret = ifd.load_byte(data) # Assert - self.assertEqual(ret, b"abc") + self.assertEqual(ret, (97, 98, 99)) def test_load_string(self): # Arrange @@ -310,38 +307,27 @@ class TestFileTiff(PillowTestCase): # Act / Assert self.assertRaises(EOFError, lambda: im.seek(1)) - def test__cvt_res_int(self): + def test__limit_rational_int(self): # Arrange - from PIL.TiffImagePlugin import _cvt_res + from PIL.TiffImagePlugin import _limit_rational value = 34 # Act - ret = _cvt_res(value) + ret = _limit_rational(value, 65536) # Assert self.assertEqual(ret, (34, 1)) - def test__cvt_res_float(self): + def test__limit_rational_float(self): # Arrange - from PIL.TiffImagePlugin import _cvt_res + from PIL.TiffImagePlugin import _limit_rational value = 22.3 # Act - ret = _cvt_res(value) + ret = _limit_rational(value, 65536) # Assert - self.assertEqual(ret, (1461452, 65536)) - - def test__cvt_res_sequence(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res - value = [0, 1] - - # Act - ret = _cvt_res(value) - - # Assert - self.assertEqual(ret, [0, 1]) + self.assertEqual(ret, (223, 10)) def test_4bit(self): # Arrange @@ -388,8 +374,8 @@ class TestFileTiff(PillowTestCase): # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION im = Image.open(filename) - self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 72) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 36) + self.assertEqual(im.tag[X_RESOLUTION], 72) + self.assertEqual(im.tag[Y_RESOLUTION], 36) def test_deprecation_warning_with_spaces(self): # Arrange: use spaces @@ -405,8 +391,8 @@ class TestFileTiff(PillowTestCase): # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION im = Image.open(filename) - self.assertEqual(im.tag.tags[X_RESOLUTION][0][0], 36) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 72) + self.assertEqual(im.tag[X_RESOLUTION], 36) + self.assertEqual(im.tag[Y_RESOLUTION], 72) if __name__ == '__main__': diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index f71db0924..51317aba3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,8 +1,10 @@ +from __future__ import division + from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags -tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) +tag_ids = {info.name: info.value for info in TiffTags.TAGS.values()} class TestFileTiffMetadata(PillowTestCase): @@ -19,7 +21,6 @@ class TestFileTiffMetadata(PillowTestCase): textdata = basetextdata + " \xff" floatdata = 12.345 doubledata = 67.89 - info = TiffImagePlugin.ImageFileDirectory() info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) @@ -45,32 +46,25 @@ class TestFileTiffMetadata(PillowTestCase): def test_read_metadata(self): img = Image.open('Tests/images/hopper_g4.tif') - known = {'YResolution': ((4294967295, 113653537),), - 'PlanarConfiguration': (1,), + known = {'YResolution': 4294967295 / 113653537, + 'PlanarConfiguration': 1, 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'RowsPerStrip': (128,), - 'ResolutionUnit': (3,), - 'PhotometricInterpretation': (0,), + 'ImageLength': 128, + 'Compression': 4, + 'FillOrder': 1, + 'RowsPerStrip': 128, + 'ResolutionUnit': 3, + 'PhotometricInterpretation': 0, 'PageNumber': (0, 1), - 'XResolution': ((4294967295, 113653537),), - 'ImageWidth': (128,), - 'Orientation': (1,), + 'XResolution': 4294967295 / 113653537, + 'ImageWidth': 128, + 'Orientation': 1, 'StripByteCounts': (1968,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,), + 'SamplesPerPixel': 1, + 'StripOffsets': (8,) } - # self.assertEqual is equivalent, - # but less helpful in telling what's wrong. - named = img.tag.named() - for tag, value in named.items(): - self.assertEqual(known[tag], value) - - for tag, value in known.items(): - self.assertEqual(value, named[tag]) + self.assertEqual(known, img.tag.named()) def test_write_metadata(self): """ Test metadata writing through the python code """ diff --git a/encode.c b/encode.c index 0f66e230b..244f5ca0d 100644 --- a/encode.c +++ b/encode.c @@ -721,7 +721,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) pos = 0; } - TRACE(("new tiff encoder %s fp: %d, filename: %s \n", compname, fp, filename)); encoder = PyImaging_EncoderNew(sizeof(TIFFSTATE)); @@ -737,11 +736,9 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) return NULL; } - // While fails on 64 bit machines, complains that pos is an int instead of a Py_ssize_t - // while (PyDict_Next(dir, &pos, &key, &value)) { - for (pos=0;posstate, (ttag_t) PyInt_AsLong(key), PyInt_AsLong(value)); - } else if(PyBytes_Check(value)) { + } else if (PyFloat_Check(value)) { + TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value))); + status = ImagingLibTiffSetField(&encoder->state, + (ttag_t) PyInt_AsLong(key), + (float)PyFloat_AsDouble(value)); + } else if (PyBytes_Check(value)) { TRACE(("Setting from Bytes: %d, %s \n", (int)PyInt_AsLong(key),PyBytes_AsString(value))); status = ImagingLibTiffSetField(&encoder->state, (ttag_t) PyInt_AsLong(key), PyBytes_AsString(value)); - } else if(PyList_Check(value)) { + } else if (PyTuple_Check(value)) { int len,i; float *floatav; int *intav; - TRACE(("Setting from List: %d \n", (int)PyInt_AsLong(key))); - len = (int)PyList_Size(value); + TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key))); + len = (int)PyTuple_Size(value); if (len) { - if (PyInt_Check(PyList_GetItem(value,0))) { + if (PyInt_Check(PyTuple_GetItem(value,0))) { TRACE((" %d elements, setting as ints \n", len)); intav = malloc(sizeof(int)*len); if (intav) { for (i=0;istate, (ttag_t) PyInt_AsLong(key), @@ -778,7 +780,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) floatav = malloc(sizeof(float)*len); if (floatav) { for (i=0;istate, (ttag_t) PyInt_AsLong(key), @@ -787,11 +789,6 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } } } - } else if (PyFloat_Check(value)) { - TRACE(("Setting from Float: %d, %f \n", (int)PyInt_AsLong(key),PyFloat_AsDouble(value))); - status = ImagingLibTiffSetField(&encoder->state, - (ttag_t) PyInt_AsLong(key), - (float)PyFloat_AsDouble(value)); } else { TRACE(("Unhandled type for key %d : %s \n", (int)PyInt_AsLong(key),