From aba7a340360f2a9bfc9204513f1924aa441748ad Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 28 Dec 2014 21:47:16 +0100 Subject: [PATCH 01/37] Fix setting of TIFF ExtraSamples tag. - force cast ExtraSamples to a list. - fix calls to ImagingLibTiffSetField to include array length. --- PIL/TiffImagePlugin.py | 2 ++ encode.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index b0e7c9639..f9f208af4 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1183,6 +1183,8 @@ 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. diff --git a/encode.c b/encode.c index 6bdb8c71a..0f66e230b 100644 --- a/encode.c +++ b/encode.c @@ -770,7 +770,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } status = ImagingLibTiffSetField(&encoder->state, (ttag_t) PyInt_AsLong(key), - intav); + len, intav); free(intav); } } else { @@ -782,7 +782,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) } status = ImagingLibTiffSetField(&encoder->state, (ttag_t) PyInt_AsLong(key), - floatav); + len, floatav); free(floatav); } } From 974bcc074be9eb791b9c66fe278f6638ad5e3e06 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 29 Dec 2014 16:48:01 +0100 Subject: [PATCH 02/37] 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), From 56a3f0f2ab83630e0df6f1a09628a8a78278b2bd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 30 Dec 2014 11:57:45 +0100 Subject: [PATCH 03/37] Fix EXIF support. --- PIL/JpegImagePlugin.py | 22 +++--------- PIL/TiffImagePlugin.py | 79 ++++++++++++++++++++++-------------------- PIL/TiffTags.py | 2 +- 3 files changed, 47 insertions(+), 56 deletions(-) diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index ffc14d2b6..9e2b35728 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -394,13 +394,6 @@ class JpegImageFile(ImageFile.ImageFile): return _getmp(self) -def _fixup(value): - # Helper function for _getexif() and _getmp() - if len(value) == 1: - return value[0] - return value - - def _getexif(self): # Extract EXIF information. This method is highly experimental, # and is likely to be replaced with something better in a future @@ -414,12 +407,10 @@ def _getexif(self): return None file = io.BytesIO(data[6:]) head = file.read(8) - exif = {} # process dictionary info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) - for key, value in info.items(): - exif[key] = _fixup(value) + exif = dict(info) # get exif extension try: # exif field 0x8769 is an offset pointer to the location @@ -431,8 +422,7 @@ def _getexif(self): else: info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) - for key, value in info.items(): - exif[key] = _fixup(value) + exif.update(info) # get gpsinfo extension try: # exif field 0x8825 is an offset pointer to the location @@ -444,9 +434,7 @@ def _getexif(self): else: info = TiffImagePlugin.ImageFileDirectory(head) info.load(file) - exif[0x8825] = gps = {} - for key, value in info.items(): - gps[key] = _fixup(value) + exif[0x8825] = dict(info) return exif @@ -464,12 +452,10 @@ def _getmp(self): file_contents = io.BytesIO(data) head = file_contents.read(8) endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' - mp = {} # process dictionary info = TiffImagePlugin.ImageFileDirectory(head) info.load(file_contents) - for key, value in info.items(): - mp[key] = _fixup(value) + mp = dict(info) # it's an error not to have a number of images try: quant = mp[0xB001] diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 99f29865f..15597847e 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -253,18 +253,28 @@ class ImageFileDirectory(collections.MutableMapping): really sure what they're doing. """ - def __init__(self, prefix=II): + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): + """Initialize an ImageFileDirectory. + + To construct an ImageFileDirectory from a real file, pass the 8-byte + magic header to the constructor. To only set the endianness, pass it + as the 'prefix' keyword argument. + + :ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :prefix: Override the endianness of the file. """ - :prefix: "II"|"MM" tiff endianness - """ - self._prefix = prefix - if prefix == MM: + if ifh[:4] not in PREFIXES: + raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) + self._prefix = prefix if prefix is not None else ifh[:2] + if self._prefix == MM: self._endian = ">" - elif prefix == II: + elif self._prefix == II: self._endian = "<" else: - raise ValueError("not a TIFF IFD") + raise SyntaxError("not a TIFF IFD") self.reset() + self.next, = self._unpack("L", ifh[4:]) prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) @@ -360,10 +370,10 @@ class ImageFileDirectory(collections.MutableMapping): def __iter__(self): return itertools.chain(list(self._tags), list(self._tagdata)) - def unpack(self, fmt, data): + def _unpack(self, fmt, data): return struct.unpack(self._endian + fmt, data) - def pack(self, fmt, *values): + def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) def _register_loader(idx, size): @@ -387,9 +397,9 @@ class ImageFileDirectory(collections.MutableMapping): TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = size, lambda self, data: ( - self.unpack("{}{}".format(len(data) // size, fmt), 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)) + b"".join(self._pack(fmt, value) for value in values)) list(map(_register_basic, [(1, "B", "byte"), (3, "H", "short"), (4, "L", "long"), @@ -411,12 +421,12 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(5, 8) def load_rational(self, data): - vals = self.unpack("{}L".format(len(data) // 4), 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(5) def write_rational(self, *values): - return b"".join(self.pack("2L", *_limit_rational(frac, 2 ** 31)) + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) for frac in values) @_register_loader(7, 1) @@ -429,12 +439,12 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data): - vals = self.unpack("{}l".format(len(data) // 4), 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)) + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) for frac in values) def load(self, fp): @@ -442,9 +452,9 @@ class ImageFileDirectory(collections.MutableMapping): self.reset() 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: + for i in range(self._unpack("H", fp.read(2))[0]): + tag, typ, count, data = self._unpack("HHL4s", fp.read(12)) + if Image.DEBUG: tagname = TAGS.get(tag, TagInfo()).name typname = TYPES.get(typ, "unknown") print("tag: %s (%d) - type: %s (%d)" % @@ -459,8 +469,8 @@ class ImageFileDirectory(collections.MutableMapping): size = count * unit_size if size > 4: here = fp.tell() - offset, = self.unpack("L", data) - if DEBUG: + offset, = self._unpack("L", data) + if Image.DEBUG: print("Tag Location: %s - Data Location: %s" % (here, offset), end=" ") fp.seek(offset) @@ -484,12 +494,16 @@ class ImageFileDirectory(collections.MutableMapping): else: print("- value:", self[tag]) - self.next, = self.unpack("L", fp.read(4)) + self.next, = self._unpack("L", fp.read(4)) def save(self, fp): + if fp.tell() == 0: # skip TIFF header on subsequent pages + # tiff header -- PIL always starts the first IFD at offset 8 + fp.write(self._prefix + self._pack("HL", 42, 8)) + # FIXME What about tagdata? - fp.write(self.pack("H", len(self._tags))) + fp.write(self._pack("H", len(self._tags))) entries = [] offset = fp.tell() + len(self._tags) * 12 + 4 @@ -521,7 +535,7 @@ class ImageFileDirectory(collections.MutableMapping): if len(data) <= 4: entries.append((tag, typ, count, data.ljust(4, b"\0"), b"")) else: - entries.append((tag, typ, count, self.pack("L", offset), data)) + 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 @@ -530,14 +544,14 @@ class ImageFileDirectory(collections.MutableMapping): if data: raise NotImplementedError( "multistrip support not yet implemented") - value = self.pack("L", self.unpack("L", value)[0] + offset) + value = self._pack("L", self._unpack("L", value)[0] + offset) entries[stripoffsets] = tag, typ, count, value, data # pass 2: write entries to file for tag, typ, count, value, data in entries: if DEBUG > 1: print(tag, typ, count, repr(value), repr(data)) - fp.write(self.pack("HHL4s", tag, typ, 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 entries @@ -573,14 +587,11 @@ class TiffImageFile(ImageFile.ImageFile): # 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]) + self.tag = self.ifd = ImageFileDirectory(ifh) # setup frame pointers - self.__first, = self.__next, = self.ifd.unpack("L", ifh[4:]) + self.__first = self.__next = self.ifd.next self.__frame = -1 self.__fp = self.fp self._frame_pos = [] @@ -991,7 +1002,7 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TIFF" % im.mode) - ifd = ImageFileDirectory(prefix) + ifd = ImageFileDirectory(prefix=prefix) compression = im.encoderinfo.get('compression', im.info.get('compression', 'raw')) @@ -1001,12 +1012,6 @@ def _save(im, fp, filename): # required for color libtiff images ifd[PLANAR_CONFIGURATION] = getattr(im, '_planar_configuration', 1) - # -- multi-page -- skip TIFF header on subsequent pages - 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.pack("HL", 42, 8)) - ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 422891aeb..f667b9143 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -129,9 +129,9 @@ TAGS = { 33432: ("Copyright", 2, 1), # FIXME add more tags here + 34665: ("ExifIFD", 3, 1), 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) } From d5b46dce960dc3a92cf40f14e01a92e52c867462 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 13 Jan 2015 23:07:41 -0800 Subject: [PATCH 04/37] Fix MPO support, and Python2.6 support. --- PIL/JpegImagePlugin.py | 9 +++++---- PIL/TiffImagePlugin.py | 14 +++++--------- PIL/TiffTags.py | 7 +++++++ Tests/test_file_tiff_metadata.py | 2 +- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 9e2b35728..8484638cb 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -36,7 +36,7 @@ import array import struct import io import warnings -from struct import unpack +from struct import unpack_from from PIL import Image, ImageFile, TiffImagePlugin, _binary from PIL.JpegPresets import presets from PIL._util import isStringType @@ -462,11 +462,12 @@ def _getmp(self): except KeyError: raise SyntaxError("malformed MP Index (no number of images)") # get MP entries + mpentries = [] try: - mpentries = [] + rawmpentries = mp[0xB002] for entrynum in range(0, quant): - rawmpentry = mp[0xB002][entrynum * 16:(entrynum + 1) * 16] - unpackedentry = unpack('{0}LLLHH'.format(endianness), rawmpentry) + unpackedentry = unpack_from( + '{0}LLLHH'.format(endianness), rawmpentries, entrynum * 16) labels = ('Attribute', 'Size', 'DataOffset', 'EntryNo1', 'EntryNo2') mpentry = dict(zip(labels, unpackedentry)) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 15597847e..8c6cf60a9 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -279,10 +279,6 @@ class ImageFileDirectory(collections.MutableMapping): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - @property - def offset(self): - return self._offset - def reset(self): self._tags = {} self._tagdata = {} @@ -302,8 +298,8 @@ class ImageFileDirectory(collections.MutableMapping): """ Returns the complete tag dictionary, with named tags where possible. """ - return {TAGS.get(code, TagInfo()).name: value - for code, value in self.items()} + return dict((TAGS.get(code, TagInfo()).name, value) + for code, value in self.items()) def __len__(self): return len(self._tagdata) + len(self._tags) @@ -397,7 +393,7 @@ class ImageFileDirectory(collections.MutableMapping): TYPES[idx] = name size = struct.calcsize("=" + fmt) _load_dispatch[idx] = size, lambda self, data: ( - self._unpack("{}{}".format(len(data) // size, fmt), data)) + self._unpack("{0}{1}".format(len(data) // size, fmt), data)) _write_dispatch[idx] = lambda self, *values: ( b"".join(self._pack(fmt, value) for value in values)) @@ -421,7 +417,7 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(5, 8) def load_rational(self, data): - vals = self._unpack("{}L".format(len(data) // 4), data) + vals = self._unpack("{0}L".format(len(data) // 4), data) return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) @@ -439,7 +435,7 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data): - vals = self._unpack("{}l".format(len(data) // 4), data) + vals = self._unpack("{0}l".format(len(data) // 4), data) return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index f667b9143..5ae5d2a93 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -130,6 +130,13 @@ TAGS = { # FIXME add more tags here 34665: ("ExifIFD", 3, 1), + + 45056: ("MPFVersion", 7, 1), + 45057: ("NumberOfImages", 4, 1), + 45058: ("MPEntry", 7, 1), + 45059: ("ImageUIDList", 7, 0), + 45060: ("TotalFrames", 4, 1), + 50741: ("MakerNoteSafety", 3, 1, {0: "Unsafe", 1: "Safe"}), 50780: ("BestQualityScale", 5, 1), 50838: ("ImageJMetaDataByteCounts", 4, 1), diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 51317aba3..78129fc58 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -4,7 +4,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags -tag_ids = {info.name: info.value for info in TiffTags.TAGS.values()} +tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS.values()) class TestFileTiffMetadata(PillowTestCase): From 38f7e23495bb7e739c755b09831dee6653d95595 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 25 Feb 2015 19:12:34 -0800 Subject: [PATCH 05/37] Include tests of #1113. --- PIL/TiffTags.py | 15 +++++++++++++++ Tests/test_file_tiff_metadata.py | 3 +-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 5ae5d2a93..6cc173332 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -131,11 +131,26 @@ TAGS = { # FIXME add more tags here 34665: ("ExifIFD", 3, 1), + # MPInfo 45056: ("MPFVersion", 7, 1), 45057: ("NumberOfImages", 4, 1), 45058: ("MPEntry", 7, 1), 45059: ("ImageUIDList", 7, 0), 45060: ("TotalFrames", 4, 1), + 45313: ("MPIndividualNum", 4, 1), + 45569: ("PanOrientation", 4, 1), + 45570: ("PanOverlap_H", 5, 1), + 45571: ("PanOverlap_V", 5, 1), + 45572: ("BaseViewpointNum", 4, 1), + 45573: ("ConvergenceAngle", 10, 1), + 45574: ("BaselineLength", 5, 1), + 45575: ("VerticalDivergence", 10, 1), + 45576: ("AxisDistance_X", 10, 1), + 45577: ("AxisDistance_Y", 10, 1), + 45578: ("AxisDistance_Z", 10, 1), + 45579: ("YawAngle", 10, 1), + 45580: ("PitchAngle", 10, 1), + 45581: ("RollAngle", 10, 1), 50741: ("MakerNoteSafety", 3, 1, {0: "Unsafe", 1: "Safe"}), 50780: ("BestQualityScale", 5, 1), diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 78129fc58..9c7a451e3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -27,7 +27,6 @@ class TestFileTiffMetadata(PillowTestCase): info[tag_ids['ImageJMetaData']] = textdata info[tag_ids['RollAngle']] = floatdata info.tagtype[tag_ids['RollAngle']] = 11 - info[tag_ids['YawAngle']] = doubledata info.tagtype[tag_ids['YawAngle']] = 12 @@ -41,7 +40,7 @@ class TestFileTiffMetadata(PillowTestCase): self.assertEqual(loaded.tag[50839], basetextdata + " ?") self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata, places=5) - self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']][0], doubledata) + self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']], doubledata) def test_read_metadata(self): img = Image.open('Tests/images/hopper_g4.tif') From 93abbd0caa035d2ff178cbef0720cd7dfcef20b0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 28 Feb 2015 19:44:38 -0800 Subject: [PATCH 06/37] Restore legacy TIFF API. To have the old API that always returns tuples, and fractions as pairs, set the `legacy_api` attribute of the IFD to True. This should alleviate concerns about backwards compatibility. --- PIL/TiffImagePlugin.py | 52 ++++++++---- Tests/helper.py | 5 -- Tests/test_file_gimpgradient.py | 2 +- Tests/test_file_libtiff.py | 52 +++++------- Tests/test_file_tiff.py | 136 ++++++++++--------------------- Tests/test_file_tiff_metadata.py | 78 ++++++++++++------ 6 files changed, 157 insertions(+), 168 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 8c6cf60a9..24a401331 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -247,10 +247,8 @@ class ImageFileDirectory(collections.MutableMapping): * 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 the private attributes self._tagdata, and in + self._tags once decoded. """ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): @@ -275,9 +273,17 @@ class ImageFileDirectory(collections.MutableMapping): raise SyntaxError("not a TIFF IFD") self.reset() self.next, = self._unpack("L", ifh[4:]) + self._legacy_api = False prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) + legacy_api = property(lambda self: self._legacy_api) + + @legacy_api.setter + def legacy_api(self, value): + if value != self._legacy_api: + self._tags.clear() + self._legacy_api = value def reset(self): self._tags = {} @@ -302,18 +308,18 @@ class ImageFileDirectory(collections.MutableMapping): for code, value in self.items()) def __len__(self): - return len(self._tagdata) + len(self._tags) + return len(set(self._tagdata) | set(self._tags)) def __getitem__(self, tag): - try: - return self._tags[tag] - except KeyError: # unpack on the fly + if tag not in self._tags: # 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] + val = self._tags[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = val, + return val def __contains__(self, tag): return tag in self._tags or tag in self._tagdata @@ -355,6 +361,8 @@ class ImageFileDirectory(collections.MutableMapping): for value in values] values = tuple(info.cvt_enum(value) for value in values) if info.length == 1: + if self.legacy_api and self.tagtype[tag] in [5, 10]: + values = values, self._tags[tag], = values else: self._tags[tag] = values @@ -364,7 +372,7 @@ class ImageFileDirectory(collections.MutableMapping): self._tagdata.pop(tag, None) def __iter__(self): - return itertools.chain(list(self._tags), list(self._tagdata)) + return iter(set(self._tagdata) | set(self._tags)) def _unpack(self, fmt, data): return struct.unpack(self._endian + fmt, data) @@ -398,10 +406,19 @@ class ImageFileDirectory(collections.MutableMapping): b"".join(self._pack(fmt, value) for value in values)) list(map(_register_basic, - [(1, "B", "byte"), (3, "H", "short"), (4, "L", "long"), + [(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(1, 1) # Basic type, except for the legacy API. + def load_byte(self, data): + return (data if self.legacy_api else + tuple(map(ord, data) if bytes is str else data)) + + @_register_writer(1) # Basic type, except for the legacy API. + def write_byte(self, data): + return data + @_register_loader(2, 1) def load_string(self, data): if data.endswith(b"\0"): @@ -418,7 +435,9 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(5, 8) def load_rational(self, data): vals = self._unpack("{0}L".format(len(data) // 4), data) - return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) + combine = lambda a, b: (a, b) if self.legacy_api else a / b + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) def write_rational(self, *values): @@ -436,7 +455,9 @@ class ImageFileDirectory(collections.MutableMapping): @_register_loader(10, 8) def load_signed_rational(self, data): vals = self._unpack("{0}l".format(len(data) // 4), data) - return tuple(num / denom for num, denom in zip(vals[::2], vals[1::2])) + combine = lambda a, b: (a, b) if self.legacy_api else a / b + return tuple(combine(num, denom) + for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) def write_signed_rational(self, *values): @@ -1026,11 +1047,14 @@ def _save(im, fp, filename): # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com if hasattr(im, 'tag'): # preserve tags from original TIFF image file + orig_api = im.tag.legacy_api + im.tag.legacy_api = False for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): if key in im.tag: ifd[key] = im.tag[key] ifd.tagtype[key] = im.tag.tagtype.get(key, None) + im.tag.legacy_api = orig_api # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech diff --git a/Tests/helper.py b/Tests/helper.py index 83d86b5d9..abb2fbd6d 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -43,11 +43,6 @@ class PillowTestCase(unittest.TestCase): else: print("=== orphaned temp file: %s" % path) - def assert_almost_equal(self, a, b, msg=None, eps=1e-6): - self.assertLess( - abs(a-b), eps, - msg or "got %r, expected %r" % (a, b)) - def assert_deep_equal(self, a, b, msg=None): try: self.assertEqual( diff --git a/Tests/test_file_gimpgradient.py b/Tests/test_file_gimpgradient.py index c54dca7c1..1653fb304 100644 --- a/Tests/test_file_gimpgradient.py +++ b/Tests/test_file_gimpgradient.py @@ -80,7 +80,7 @@ class TestImage(PillowTestCase): ret = GimpGradientFile.sphere_increasing(middle, pos) # Assert - self.assert_almost_equal(ret, 0.9682458365518543) + self.assertAlmostEqual(ret, 0.9682458365518543) def test_sphere_decreasing(self): # Arrange diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 703ba3015..2d0f2b041 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -2,7 +2,11 @@ from __future__ import print_function from helper import unittest, PillowTestCase, hopper, py3 import io +<<<<<<< HEAD import logging +======= +import itertools +>>>>>>> Restore legacy TIFF API. import os from PIL import Image, TiffImagePlugin @@ -123,41 +127,27 @@ class TestFileLibTiff(LibTiffTestCase): def test_write_metadata(self): """ Test metadata writing through libtiff """ - img = Image.open('Tests/images/hopper_g4.tif') - f = self.tempfile('temp.tiff') + for legacy_api in [False, True]: + img = Image.open('Tests/images/hopper_g4.tif') + img.tag.legacy_api = legacy_api + f = self.tempfile('temp.tiff') - img.save(f, tiffinfo=img.tag) + img.save(f, tiffinfo=img.tag) + original = img.tag.named() - loaded = Image.open(f) + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', + 'PhotometricInterpretation'] - original = img.tag.named() - reloaded = loaded.tag.named() + loaded = Image.open(f) + loaded.tag.legacy_api = legacy_api + reloaded = loaded.tag.named() - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - 'StripByteCounts', 'RowsPerStrip', - 'PageNumber', 'PhotometricInterpretation'] - - for tag, value in reloaded.items(): - if tag not in ignored: - if tag.endswith('Resolution'): - self.assert_almost_equal( - original[tag], value, - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - original[tag], value, "%s didn't roundtrip" % tag) - - for tag, value in original.items(): - if tag not in ignored: - if tag.endswith('Resolution'): - self.assert_almost_equal( - original[tag], value, - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - value, reloaded[tag], "%s didn't roundtrip" % tag) + for tag, value in itertools.chain(reloaded.items(), original.items()): + if tag not in ignored: + val = original[tag] + self.assertEqual(val, value, msg="%s didn't roundtrip" % tag) def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 147507f54..4e3331058 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -73,12 +73,18 @@ class TestFileTiff(PillowTestCase): def test_xyres_tiff(self): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" - im = Image.open(filename) - # Try to read a file where X,Y_RESOLUTION are ints - im.tag[X_RESOLUTION] = (72,) - im.tag[Y_RESOLUTION] = (72,) - im._setup() - self.assertEqual(im.info['dpi'], (72., 72.)) + for legacy_api in [False, True]: + im = Image.open(filename) + im.tag.legacy_api = legacy_api + if legacy_api: + assert isinstance(im.tag[X_RESOLUTION][0], tuple) + assert isinstance(im.tag[Y_RESOLUTION][0], tuple) + # Try to read a file where X,Y_RESOLUTION are ints + im.tag[X_RESOLUTION] = (72,) + im.tag[Y_RESOLUTION] = (72,) + im.tag.legacy_api = False # _setup assumes the new API. + im._setup() + self.assertEqual(im.info['dpi'], (72., 72.)) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -204,7 +210,6 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.convert('RGB').getpixel((0, 0)), (0, 0, 255)) def test___str__(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) @@ -217,127 +222,81 @@ class TestFileTiff(PillowTestCase): def test_as_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - im = Image.open(filename) - - # Act - ret = im.ifd.as_dict() - - # Assert - 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: 72.0, 283: 72.0, 284: 1}) + for legacy_api in [False, True]: + im = Image.open(filename) + im.tag.legacy_api = legacy_api + self.assertEqual( + im.tag.as_dict(), + {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,)} if legacy_api else + {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 filename = "Tests/images/pil136.tiff" im = Image.open(filename) len_before = len(im.ifd.as_dict()) - - # Act del im.ifd[256] - - # Assert len_after = len(im.ifd.as_dict()) self.assertEqual(len_before, len_after + 1) def test_load_byte(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() - data = b"abc" - - # Act - ret = ifd.load_byte(data) - - # Assert - self.assertEqual(ret, (97, 98, 99)) + for legacy_api in [False, True]: + ifd = TiffImagePlugin.ImageFileDirectory() + ifd.legacy_api = legacy_api + data = b"abc" + ret = ifd.load_byte(data) + self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): - # Arrange ifd = TiffImagePlugin.ImageFileDirectory() data = b"abc\0" - - # Act ret = ifd.load_string(data) - - # Assert self.assertEqual(ret, "abc") def test_load_float(self): - # Arrange ifd = TiffImagePlugin.ImageFileDirectory() data = b"abcdabcd" - - # Act ret = ifd.load_float(data) - - # Assert self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): - # Arrange ifd = TiffImagePlugin.ImageFileDirectory() data = b"abcdefghabcdefgh" - - # Act ret = ifd.load_double(data) - - # Assert self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) def test_seek(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) - - # Act im.seek(-1) - - # Assert self.assertEqual(im.tell(), 0) def test_seek_eof(self): - # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) self.assertEqual(im.tell(), 0) - - # Act / Assert self.assertRaises(EOFError, lambda: im.seek(1)) def test__limit_rational_int(self): - # Arrange from PIL.TiffImagePlugin import _limit_rational value = 34 - - # Act ret = _limit_rational(value, 65536) - - # Assert self.assertEqual(ret, (34, 1)) def test__limit_rational_float(self): - # Arrange from PIL.TiffImagePlugin import _limit_rational value = 22.3 - - # Act ret = _limit_rational(value, 65536) - - # Assert self.assertEqual(ret, (223, 10)) def test_4bit(self): - # Arrange test_file = "Tests/images/hopper_gray_4bpp.tif" original = hopper("L") - - # Act im = Image.open(test_file) - - # Assert self.assertEqual(im.size, (128, 128)) self.assertEqual(im.mode, "L") self.assert_image_similar(im, original, 7.3) @@ -347,52 +306,45 @@ class TestFileTiff(PillowTestCase): # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. # The second is the total number of pages, zero means not available. - - # Arrange outfile = self.tempfile("temp.tif") - # Created by printing a page in Chrome to PDF, then: # /usr/bin/gs -q -sDEVICE=tiffg3 -sOutputFile=total-pages-zero.tif # -dNOPAUSE /tmp/test.pdf -c quit infile = "Tests/images/total-pages-zero.tif" im = Image.open(infile) - - # Act / Assert # Should not divide by zero im.save(outfile) def test_with_underscores(self): - # Arrange: use underscores kwargs = {'resolution_unit': 'inch', 'x_resolution': 72, 'y_resolution': 36} filename = self.tempfile("temp.tif") - - # Act hopper("RGB").save(filename, **kwargs) - - # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - im = Image.open(filename) - self.assertEqual(im.tag[X_RESOLUTION], 72) - self.assertEqual(im.tag[Y_RESOLUTION], 36) + for legacy_api in [False, True]: + im = Image.open(filename) + im.tag.legacy_api = legacy_api + self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api + else im.tag[X_RESOLUTION], 72) + self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api + else im.tag[Y_RESOLUTION], 36) def test_deprecation_warning_with_spaces(self): - # Arrange: use spaces kwargs = {'resolution unit': 'inch', 'x resolution': 36, 'y resolution': 72} filename = self.tempfile("temp.tif") - - # Act self.assert_warning(DeprecationWarning, lambda: hopper("RGB").save(filename, **kwargs)) - - # Assert from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - im = Image.open(filename) - self.assertEqual(im.tag[X_RESOLUTION], 36) - self.assertEqual(im.tag[Y_RESOLUTION], 72) + for legacy_api in [False, True]: + im = Image.open(filename) + im.tag.legacy_api = legacy_api + self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api + else im.tag[X_RESOLUTION], 36) + self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api + else 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 9c7a451e3..df1934482 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -34,36 +34,64 @@ class TestFileTiffMetadata(PillowTestCase): img.save(f, tiffinfo=info) - loaded = Image.open(f) + for legacy_api in [False, True]: + loaded = Image.open(f) + loaded.tag.legacy_api = legacy_api + + self.assertEqual(loaded.tag[50838], + (len(basetextdata + " ?"),) if legacy_api else len(basetextdata + " ?")) + self.assertEqual(loaded.tag[50839], basetextdata + " ?") + loaded_float = loaded.tag[tag_ids['RollAngle']] + if legacy_api: + loaded_float = loaded_float[0] + self.assertAlmostEqual(loaded_float, floatdata, places=5) + loaded_double = loaded.tag[tag_ids['YawAngle']] + if legacy_api: + loaded_double = loaded_double[0] + self.assertAlmostEqual(loaded_double, doubledata) - self.assertEqual(loaded.tag[50838], (len(basetextdata + " ?"),)) - self.assertEqual(loaded.tag[50839], basetextdata + " ?") - self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata, - places=5) - self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']], doubledata) def test_read_metadata(self): - img = Image.open('Tests/images/hopper_g4.tif') + for legacy_api in [False, True]: + img = Image.open('Tests/images/hopper_g4.tif') + img.tag.legacy_api = legacy_api - known = {'YResolution': 4294967295 / 113653537, - 'PlanarConfiguration': 1, - 'BitsPerSample': (1,), - 'ImageLength': 128, - 'Compression': 4, - 'FillOrder': 1, - 'RowsPerStrip': 128, - 'ResolutionUnit': 3, - 'PhotometricInterpretation': 0, - 'PageNumber': (0, 1), - 'XResolution': 4294967295 / 113653537, - 'ImageWidth': 128, - 'Orientation': 1, - 'StripByteCounts': (1968,), - 'SamplesPerPixel': 1, - 'StripOffsets': (8,) - } + known = {'YResolution': ((4294967295, 113653537),), + 'PlanarConfiguration': (1,), + 'BitsPerSample': (1,), + 'ImageLength': (128,), + 'Compression': (4,), + 'FillOrder': (1,), + 'RowsPerStrip': (128,), + 'ResolutionUnit': (3,), + 'PhotometricInterpretation': (0,), + 'PageNumber': (0, 1), + 'XResolution': ((4294967295, 113653537),), + 'ImageWidth': (128,), + 'Orientation': (1,), + 'StripByteCounts': (1968,), + 'SamplesPerPixel': (1,), + 'StripOffsets': (8,) + } if legacy_api else { + 'YResolution': 4294967295 / 113653537, + 'PlanarConfiguration': 1, + 'BitsPerSample': (1,), + 'ImageLength': 128, + 'Compression': 4, + 'FillOrder': 1, + 'RowsPerStrip': 128, + 'ResolutionUnit': 3, + 'PhotometricInterpretation': 0, + 'PageNumber': (0, 1), + 'XResolution': 4294967295 / 113653537, + 'ImageWidth': 128, + 'Orientation': 1, + 'StripByteCounts': (1968,), + 'SamplesPerPixel': 1, + 'StripOffsets': (8,) + } - self.assertEqual(known, img.tag.named()) + self.assertEqual(known, img.tag.named()) def test_write_metadata(self): """ Test metadata writing through the python code """ From 6309bfe92110be8b0cd0b588ce36ac13cd027c5c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 16 Jun 2015 14:16:56 -0700 Subject: [PATCH 07/37] Support too long ExtraSamples. Some programs generate SamplesPerPixel entries in ExtraSamples instead of SamplesPerPixel-3, cf. #1227. This is a stopgap measure to support them. One could also decide to add generic code to always support having SamplesPerPixel entries (by dropping the first 3). --- PIL/TiffImagePlugin.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 24a401331..2fa9388eb 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -157,6 +157,7 @@ OPEN_INFO = { (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, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBA"), # OSX Grab (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), @@ -185,10 +186,12 @@ OPEN_INFO = { (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), ()): ("RGBA", "RGBA"), # missing ExtraSamples (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, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBA"), # OSX Grab (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), @@ -705,9 +708,9 @@ class TiffImageFile(ImageFile.ImageFile): args = rawmode elif compression == "tiff_lzw": args = rawmode - if 317 in self.tag: + if PREDICTOR in self.tag: # Section 14: Differencing Predictor - self.decoderconfig = (self.tag[PREDICTOR][0],) + self.decoderconfig = (self.tag[PREDICTOR],) if ICCPROFILE in self.tag: self.info['icc_profile'] = self.tag[ICCPROFILE] From 1b9b3749ca23cf723229d23535b327f28600138e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 16 Jun 2015 14:27:02 -0700 Subject: [PATCH 08/37] Reorder OPEN_INFO to cover both endiannesses. By interleaving little and big-endian entries we make sure entries exist for both cases. Some additional entries created when the big-endian was missing. I am not sure of what entry to create for the big-endian, 4-bit case (what is the order of the two entries within the byte?). --- PIL/TiffImagePlugin.py | 68 +++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 2fa9388eb..40165f101 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -134,77 +134,77 @@ OPEN_INFO = { # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample, # ExtraSamples) => mode, rawmode (II, 0, (1,), 1, (1,), ()): ("1", "1;I"), + (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"), (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"), + (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"), (II, 0, (1,), 1, (8,), ()): ("L", "L;I"), + (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"), (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"), + (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"), + (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"), (II, 1, (1,), 1, (1,), ()): ("1", "1"), + (MM, 1, (1,), 1, (1,), ()): ("1", "1"), (II, 1, (1,), 1, (4,), ()): ("L", "L;4"), + # ? (II, 1, (1,), 2, (1,), ()): ("1", "1;R"), + (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), + (MM, 1, (1,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), + (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), + (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"), + (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"), (II, 1, (2,), 1, (16,), ()): ("I;16S", "I;16S"), + (MM, 1, (2,), 1, (16,), ()): ("I;16BS", "I;16BS"), (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, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBA"), # OSX Grab - (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"), + (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"), (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"), + (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"), + (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"), + (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"), # missing ExtraSamples + (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"), + (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"), + (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"), + (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"), # Corel Draw 10 + (II, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBA"), # OSX Grab (MM, 2, (1, 1, 1, 1), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBA"), # OSX Grab + (II, 3, (1,), 1, (1,), ()): ("P", "P;1"), (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"), + (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"), (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"), + (II, 3, (1,), 1, (2,), ()): ("P", "P;2"), (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"), + (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"), (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"), + (II, 3, (1,), 1, (4,), ()): ("P", "P;4"), (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"), + (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"), (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"), + (II, 3, (1,), 1, (8,), ()): ("P", "P"), (MM, 3, (1,), 1, (8,), ()): ("P", "P"), + (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"), + (II, 3, (1,), 2, (8,), ()): ("P", "P;R"), (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"), + (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"), + (II, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), (MM, 6, (1,), 1, (8, 8, 8), ()): ("YCbCr", "YCbCr"), + (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), - } PREFIXES = [b"MM\000\052", b"II\052\000", b"II\xBC\000"] From ba7b8d1708079a7026d640f1f7eb06a91d24ef93 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 29 Jun 2015 15:37:58 -0700 Subject: [PATCH 09/37] Fail on invalid EXIF, reverting ed2cca1. --- Tests/test_file_tiff.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4e3331058..c2c7c0eec 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -93,16 +93,12 @@ class TestFileTiff(PillowTestCase): lambda: TiffImagePlugin.TiffImageFile(invalid_file)) def test_bad_exif(self): - try: - Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() - except struct.error: - self.fail( - "Bad EXIF data passed incorrect values to _binary unpack") + image = Image.open('Tests/images/hopper_bad_exif.jpg') + self.assertRaises(Exception, image._getexif) def test_save_unsupported_mode(self): im = hopper("HSV") outfile = self.tempfile("temp.tif") - self.assertRaises(IOError, lambda: im.save(outfile)) def test_little_endian(self): From c113e8f7cdbfb66763273f49145598d56aa36cd4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 30 Jun 2015 18:29:29 -0700 Subject: [PATCH 10/37] libtiff's rational precision is limited to C floats. --- Tests/test_file_libtiff.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 2d0f2b041..13823d28f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,6 +1,7 @@ from __future__ import print_function from helper import unittest, PillowTestCase, hopper, py3 +from ctypes import c_float import io <<<<<<< HEAD import logging @@ -144,10 +145,23 @@ class TestFileLibTiff(LibTiffTestCase): loaded.tag.legacy_api = legacy_api reloaded = loaded.tag.named() - for tag, value in itertools.chain(reloaded.items(), original.items()): + for tag, value in itertools.chain(reloaded.items(), + original.items()): if tag not in ignored: val = original[tag] - self.assertEqual(val, value, msg="%s didn't roundtrip" % tag) + if tag.endswith('Resolution'): + if legacy_api: + self.assertEqual( + c_float(val[0][0] / val[0][1]).value, + c_float(value[0][0] / value[0][1]).value, + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + c_float(val).value, c_float(value).value, + msg="%s didn't roundtrip" % tag) + else: + self.assertEqual( + val, value, msg="%s didn't roundtrip" % tag) def test_g3_compression(self): i = Image.open('Tests/images/hopper_g4_500.tif') From 0c942911d99dea2ac9c736ae95b292286e937f21 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 10 Sep 2015 08:33:14 -0700 Subject: [PATCH 11/37] fixed the rebase --- PIL/TiffImagePlugin.py | 4 ++-- Tests/test_file_libtiff.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 40165f101..b64c9e9bc 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -474,7 +474,7 @@ class ImageFileDirectory(collections.MutableMapping): for i in range(self._unpack("H", fp.read(2))[0]): tag, typ, count, data = self._unpack("HHL4s", fp.read(12)) - if Image.DEBUG: + if DEBUG: tagname = TAGS.get(tag, TagInfo()).name typname = TYPES.get(typ, "unknown") print("tag: %s (%d) - type: %s (%d)" % @@ -490,7 +490,7 @@ class ImageFileDirectory(collections.MutableMapping): if size > 4: here = fp.tell() offset, = self._unpack("L", data) - if Image.DEBUG: + if DEBUG: print("Tag Location: %s - Data Location: %s" % (here, offset), end=" ") fp.seek(offset) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 13823d28f..33d77f558 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -3,11 +3,8 @@ from helper import unittest, PillowTestCase, hopper, py3 from ctypes import c_float import io -<<<<<<< HEAD import logging -======= import itertools ->>>>>>> Restore legacy TIFF API. import os from PIL import Image, TiffImagePlugin From 9bb4c516299a92c3d2c10cc4b94dd4d6ec84829e Mon Sep 17 00:00:00 2001 From: wiredfool Date: Thu, 10 Sep 2015 08:49:37 -0700 Subject: [PATCH 12/37] module level default api level --- PIL/TiffImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index b64c9e9bc..64efb4d9b 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -63,6 +63,7 @@ DEBUG = False # Needs to be merged with the new logging approach. # Set these to true to force use of libtiff for reading or writing. READ_LIBTIFF = False WRITE_LIBTIFF = False +IFD_LEGACY_API = True II = b"II" # little-endian (Intel style) MM = b"MM" # big-endian (Motorola style) @@ -276,7 +277,7 @@ class ImageFileDirectory(collections.MutableMapping): raise SyntaxError("not a TIFF IFD") self.reset() self.next, = self._unpack("L", ifh[4:]) - self._legacy_api = False + self._legacy_api = IFD_LEGACY_API prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) From 47a963c2a44130bf95f03ca3a0e2c41218ff2c3c Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 11 Sep 2015 10:09:14 -0700 Subject: [PATCH 13/37] Legacy/versioned interface --- PIL/JpegImagePlugin.py | 8 +-- PIL/TiffImagePlugin.py | 114 ++++++++++++++++++++++++---------------- Tests/test_file_tiff.py | 96 +++++++++++++++++++-------------- 3 files changed, 130 insertions(+), 88 deletions(-) diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 8484638cb..3d01c5fc1 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -408,7 +408,7 @@ def _getexif(self): file = io.BytesIO(data[6:]) head = file.read(8) # process dictionary - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(file) exif = dict(info) # get exif extension @@ -420,7 +420,7 @@ def _getexif(self): except (KeyError, TypeError): pass else: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(file) exif.update(info) # get gpsinfo extension @@ -432,7 +432,7 @@ def _getexif(self): except (KeyError, TypeError): pass else: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(file) exif[0x8825] = dict(info) return exif @@ -453,7 +453,7 @@ def _getmp(self): head = file_contents.read(8) endianness = '>' if head[:4] == b'\x4d\x4d\x00\x2a' else '<' # process dictionary - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(file_contents) mp = dict(info) # it's an error not to have a number of images diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 64efb4d9b..de2381b34 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -227,7 +227,7 @@ def _limit_rational(val, max_val): _load_dispatch = {} _write_dispatch = {} -class ImageFileDirectory(collections.MutableMapping): +class ImageFileDirectory_v2(collections.MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -277,8 +277,8 @@ class ImageFileDirectory(collections.MutableMapping): raise SyntaxError("not a TIFF IFD") self.reset() self.next, = self._unpack("L", ifh[4:]) - self._legacy_api = IFD_LEGACY_API - + self._legacy_api = False + prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) legacy_api = property(lambda self: self._legacy_api) @@ -585,14 +585,33 @@ class ImageFileDirectory(collections.MutableMapping): return offset -ImageFileDirectory._load_dispatch = _load_dispatch -ImageFileDirectory._write_dispatch = _write_dispatch +ImageFileDirectory_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._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]) + setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1]) + setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx]) del _load_dispatch, _write_dispatch, idx, name +#Legacy ImageFileDirectory support. +class ImageFileDirectory_v1(ImageFileDirectory_v2): + def __init__(self, *args, **kwargs): + ImageFileDirectory_v2.__init__(self, *args, **kwargs) + self.legacy_api=True + #insert deprecation warning here. + + tags = property(lambda self: self._tags) + tagdata = property(lambda self: self._tagdata) + + @classmethod + def from_v2(cls, original): + ifd = cls(prefix=original.prefix) + ifd._tagdata = original._tagdata + ifd.tagtype = original.tagtype + return ifd + +# undone -- switch this pointer when IFD_LEGACY_API == False +ImageFileDirectory = ImageFileDirectory_v1 ## # Image plugin for TIFF files. @@ -609,10 +628,13 @@ class TiffImageFile(ImageFile.ImageFile): ifh = self.fp.read(8) # image file directory (tag dictionary) - self.tag = self.ifd = ImageFileDirectory(ifh) + self.tag_v2 = ImageFileDirectory_v2(ifh) + + # legacy tag/ifd entries will be filled in later + self.tag = self.ifd = None # setup frame pointers - self.__first = self.__next = self.ifd.next + self.__first = self.__next = self.tag_v2.next self.__frame = -1 self.__fp = self.fp self._frame_pos = [] @@ -678,11 +700,13 @@ class TiffImageFile(ImageFile.ImageFile): self._frame_pos.append(self.__next) if DEBUG: print("Loading tags, location: %s" % self.fp.tell()) - self.tag.load(self.fp) - self.__next = self.tag.next + self.tag_v2.load(self.fp) + self.__next = self.tag_v2.next self.__frame += 1 self.fp.seek(self._frame_pos[frame]) - self.tag.load(self.fp) + self.tag_v2.load(self.fp) + # fill the legacy tag/ifd entries + self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2) self.__frame = frame self._setup() @@ -701,20 +725,20 @@ class TiffImageFile(ImageFile.ImageFile): args = (rawmode, 0, 1) elif compression == "jpeg": args = rawmode, "" - if JPEGTABLES in self.tag: + if JPEGTABLES in self.tag_v2: # Hack to handle abbreviated JPEG headers # FIXME This will fail with more than one value - self.tile_prefix, = self.tag[JPEGTABLES] + self.tile_prefix, = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": args = rawmode - if PREDICTOR in self.tag: + if PREDICTOR in self.tag_v2: # Section 14: Differencing Predictor - self.decoderconfig = (self.tag[PREDICTOR],) + self.decoderconfig = (self.tag_v2[PREDICTOR],) - if ICCPROFILE in self.tag: - self.info['icc_profile'] = self.tag[ICCPROFILE] + if ICCPROFILE in self.tag_v2: + self.info['icc_profile'] = self.tag_v2[ICCPROFILE] return args @@ -737,7 +761,7 @@ class TiffImageFile(ImageFile.ImageFile): # (self._compression, (extents tuple), # 0, (rawmode, self._compression, fp)) extents = self.tile[0][1] - args = self.tile[0][3] + (self.ifd.offset,) + args = self.tile[0][3] + (self.tag_v2.offset,) decoder = Image._getdecoder(self.mode, 'libtiff', args, self.decoderconfig) try: @@ -790,18 +814,18 @@ class TiffImageFile(ImageFile.ImageFile): def _setup(self): "Setup this image object based on current tags" - if 0xBC01 in self.tag: + if 0xBC01 in self.tag_v2: raise IOError("Windows Media Photo files not yet supported") # extract relevant tags - self._compression = COMPRESSION_INFO[self.tag.get(COMPRESSION, 1)] - self._planar_configuration = self.tag.get(PLANAR_CONFIGURATION, 1) + self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)] + self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1) # photometric is a required tag, but not everyone is reading # the specification - photo = self.tag.get(PHOTOMETRIC_INTERPRETATION, 0) + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - fillorder = self.tag.get(FILLORDER, 1) + fillorder = self.tag_v2.get(FILLORDER, 1) if DEBUG: print("*** Summary ***") @@ -811,20 +835,20 @@ class TiffImageFile(ImageFile.ImageFile): print("- fill_order:", fillorder) # size - xsize = self.tag.get(IMAGEWIDTH) - ysize = self.tag.get(IMAGELENGTH) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) self.size = xsize, ysize if DEBUG: print("- size:", self.size) - format = self.tag.get(SAMPLEFORMAT, (1,)) + format = self.tag_v2.get(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, ()) + self.tag_v2.prefix, photo, format, fillorder, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) ) if DEBUG: print("format key:", key) @@ -841,8 +865,8 @@ class TiffImageFile(ImageFile.ImageFile): self.info["compression"] = self._compression - xres = self.tag.get(X_RESOLUTION, (1, 1)) - yres = self.tag.get(Y_RESOLUTION, (1, 1)) + xres = self.tag_v2.get(X_RESOLUTION, (1, 1)) + yres = self.tag_v2.get(Y_RESOLUTION, (1, 1)) if xres and not isinstance(xres, tuple): xres = (xres, 1.) @@ -851,7 +875,7 @@ class TiffImageFile(ImageFile.ImageFile): if xres and yres: xres = xres[0] / (xres[1] or 1) yres = yres[0] / (yres[1] or 1) - resunit = self.tag.get(RESOLUTION_UNIT, 1) + resunit = self.tag_v2.get(RESOLUTION_UNIT, 1) if resunit == 2: # dots per inch self.info["dpi"] = xres, yres elif resunit == 3: # dots per centimeter. convert to dpi @@ -862,10 +886,10 @@ class TiffImageFile(ImageFile.ImageFile): # build tile descriptors x = y = l = 0 self.tile = [] - if STRIPOFFSETS in self.tag: + if STRIPOFFSETS in self.tag_v2: # striped image - offsets = self.tag[STRIPOFFSETS] - h = self.tag.get(ROWSPERSTRIP, ysize) + offsets = self.tag_v2[STRIPOFFSETS] + h = self.tag_v2.get(ROWSPERSTRIP, ysize) w = self.size[0] if READ_LIBTIFF or self._compression in ["tiff_ccitt", "group3", "group4", "tiff_jpeg", @@ -912,9 +936,9 @@ class TiffImageFile(ImageFile.ImageFile): # https://github.com/python-pillow/Pillow/issues/279 if fillorder == 2: key = ( - self.tag.prefix, photo, format, 1, - self.tag.get(BITSPERSAMPLE, (1,)), - self.tag.get(EXTRASAMPLES, ()) + self.tag_v2.prefix, photo, format, 1, + self.tag_v2.get(BITSPERSAMPLE, (1,)), + self.tag_v2.get(EXTRASAMPLES, ()) ) if DEBUG: print("format key:", key) @@ -952,12 +976,12 @@ class TiffImageFile(ImageFile.ImageFile): x = y = 0 l += 1 a = None - elif TILEOFFSETS in self.tag: + elif TILEOFFSETS in self.tag_v2: # tiled image - w = self.tag.get(322) - h = self.tag.get(323) + w = self.tag_v2.get(322) + h = self.tag_v2.get(323) a = None - for o in self.tag[TILEOFFSETS]: + for o in self.tag_v2[TILEOFFSETS]: if not a: a = self._decoder(rawmode, l) # FIXME: this doesn't work if the image size @@ -981,7 +1005,7 @@ class TiffImageFile(ImageFile.ImageFile): # fixup palette descriptor if self.mode == "P": - palette = [o8(b // 256) for b in self.tag[COLORMAP]] + palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) # # -------------------------------------------------------------------- @@ -1023,7 +1047,7 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TIFF" % im.mode) - ifd = ImageFileDirectory(prefix=prefix) + ifd = ImageFileDirectory_v2(prefix=prefix) compression = im.encoderinfo.get('compression', im.info.get('compression', 'raw')) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c2c7c0eec..f5df2c301 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -73,18 +73,24 @@ class TestFileTiff(PillowTestCase): def test_xyres_tiff(self): from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION filename = "Tests/images/pil168.tif" - for legacy_api in [False, True]: - im = Image.open(filename) - im.tag.legacy_api = legacy_api - if legacy_api: - assert isinstance(im.tag[X_RESOLUTION][0], tuple) - assert isinstance(im.tag[Y_RESOLUTION][0], tuple) - # Try to read a file where X,Y_RESOLUTION are ints - im.tag[X_RESOLUTION] = (72,) - im.tag[Y_RESOLUTION] = (72,) - im.tag.legacy_api = False # _setup assumes the new API. - im._setup() - self.assertEqual(im.info['dpi'], (72., 72.)) + im = Image.open(filename) + + #legacy api + self.assert_(isinstance(im.tag[X_RESOLUTION][0], tuple)) + self.assert_(isinstance(im.tag[Y_RESOLUTION][0], tuple)) + + #v2 api + self.assert_(isinstance(im.tag_v2[X_RESOLUTION], float)) + self.assert_(isinstance(im.tag_v2[Y_RESOLUTION], float)) + + self.assertEqual(im.info['dpi'], (72., 72.)) + + def xtest_int_resolution(self): + # Try to read a file where X,Y_RESOLUTION are ints + im.tag[X_RESOLUTION] = (72,) + im.tag[Y_RESOLUTION] = (72,) + im._setup() + self.assertEqual(im.info['dpi'], (72., 72.)) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -94,6 +100,7 @@ class TestFileTiff(PillowTestCase): def test_bad_exif(self): image = Image.open('Tests/images/hopper_bad_exif.jpg') + image._getexif() self.assertRaises(Exception, image._getexif) def test_save_unsupported_mode(self): @@ -218,18 +225,21 @@ class TestFileTiff(PillowTestCase): def test_as_dict(self): # Arrange filename = "Tests/images/pil136.tiff" - for legacy_api in [False, True]: - im = Image.open(filename) - im.tag.legacy_api = legacy_api - self.assertEqual( + im = Image.open(filename) + # v2 interface + self.assertEqual( + im.tag_v2.as_dict(), + {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}) + + # legacy interface + self.assertEqual( im.tag.as_dict(), {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,)} if legacy_api else - {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}) + 283: ((720000, 10000),), 284: (1,)}) def test__delitem__(self): filename = "Tests/images/pil136.tiff" @@ -241,26 +251,26 @@ class TestFileTiff(PillowTestCase): def test_load_byte(self): for legacy_api in [False, True]: - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd.legacy_api = legacy_api data = b"abc" ret = ifd.load_byte(data) self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" ret = ifd.load_string(data) self.assertEqual(ret, "abc") def test_load_float(self): - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" ret = ifd.load_float(data) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" ret = ifd.load_double(data) self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) @@ -297,7 +307,10 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "L") self.assert_image_similar(im, original, 7.3) - def test_page_number_x_0(self): +### +# UNDONE +### Segfaulting + def xtest_page_number_x_0(self): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. @@ -318,13 +331,15 @@ class TestFileTiff(PillowTestCase): filename = self.tempfile("temp.tif") hopper("RGB").save(filename, **kwargs) from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - for legacy_api in [False, True]: - im = Image.open(filename) - im.tag.legacy_api = legacy_api - self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api - else im.tag[X_RESOLUTION], 72) - self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api - else im.tag[Y_RESOLUTION], 36) + im = Image.open(filename) + + # legacy interface + self.assertEqual(im.tag[X_RESOLUTION][0][0], 72) + self.assertEqual(im.tag[Y_RESOLUTION][0][0], 36) + + # v2 interface + self.assertEqual(im.tag_v2[X_RESOLUTION], 72) + self.assertEqual(im.tag_v2[Y_RESOLUTION], 36) def test_deprecation_warning_with_spaces(self): kwargs = {'resolution unit': 'inch', @@ -334,13 +349,16 @@ class TestFileTiff(PillowTestCase): self.assert_warning(DeprecationWarning, lambda: hopper("RGB").save(filename, **kwargs)) from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION - for legacy_api in [False, True]: - im = Image.open(filename) - im.tag.legacy_api = legacy_api - self.assertEqual(im.tag[X_RESOLUTION][0][0] if legacy_api - else im.tag[X_RESOLUTION], 36) - self.assertEqual(im.tag[Y_RESOLUTION][0][0] if legacy_api - else im.tag[Y_RESOLUTION], 72) + + im = Image.open(filename) + + # legacy interface + self.assertEqual(im.tag[X_RESOLUTION][0][0], 36) + self.assertEqual(im.tag[Y_RESOLUTION][0][0], 72) + + # v2 interface + self.assertEqual(im.tag_v2[X_RESOLUTION], 36) + self.assertEqual(im.tag_v2[Y_RESOLUTION], 72) if __name__ == '__main__': From 5f9fff02155957287154787a9d1d235845108cea Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 11 Sep 2015 10:45:10 -0700 Subject: [PATCH 14/37] Restoring bad exif handling --- PIL/TiffImagePlugin.py | 90 +++++++++++++++++++++++------------------ Tests/test_file_tiff.py | 8 ++-- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index de2381b34..4638acd8f 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -468,55 +468,67 @@ class ImageFileDirectory_v2(collections.MutableMapping): return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 30)) for frac in values) + def _ensure_read(self, fp, size): + ret = fp.read(size) + if len(ret) != size: + raise IOError("Corrupt EXIF data. " + + "Expecting to read %d bytes but only got %d. " % + (size, len(ret))) + return ret + def load(self, fp): self.reset() 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: - tagname = TAGS.get(tag, TagInfo()).name - typname = TYPES.get(typ, "unknown") - print("tag: %s (%d) - type: %s (%d)" % - (tagname, tag, typname, typ), end=" ") - - try: - unit_size, handler = self._load_dispatch[typ] - except KeyError: + try: + for i in range(self._unpack("H", self._ensure_read(fp,2))[0]): + tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp,12)) if DEBUG: - print("- unsupported type", typ) - continue # ignore unsupported type - size = count * unit_size - if size > 4: - here = fp.tell() - offset, = self._unpack("L", data) - if DEBUG: - print("Tag Location: %s - Data Location: %s" % - (here, offset), end=" ") - fp.seek(offset) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = data[:size] + tagname = TAGS.get(tag, TagInfo()).name + typname = TYPES.get(typ, "unknown") + print("tag: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") - if len(data) != size: - warnings.warn("Possibly corrupt EXIF data. " - "Expecting to read %d bytes but only got %d. " - "Skipping tag %s" % (size, len(data), tag)) - continue - - self._tagdata[tag] = data - self.tagtype[tag] = typ - - if DEBUG: - if size > 32: - print("- value: " % size) + try: + unit_size, handler = self._load_dispatch[typ] + except KeyError: + if DEBUG: + print("- unsupported type", typ) + continue # ignore unsupported type + size = count * unit_size + if size > 4: + here = fp.tell() + offset, = self._unpack("L", data) + if DEBUG: + print("Tag Location: %s - Data Location: %s" % + (here, offset), end=" ") + fp.seek(offset) + data = ImageFile._safe_read(fp, size) + fp.seek(here) else: - print("- value:", self[tag]) + data = data[:size] - self.next, = self._unpack("L", fp.read(4)) + if len(data) != size: + warnings.warn("Possibly corrupt EXIF data. " + "Expecting to read %d bytes but only got %d. " + "Skipping tag %s" % (size, len(data), tag)) + continue + self._tagdata[tag] = data + self.tagtype[tag] = typ + + if DEBUG: + if size > 32: + print("- value: " % size) + else: + print("- value:", self[tag]) + + self.next, = self._unpack("L", self._ensure_read(fp,4)) + except IOError as msg: + warnings.warn(str(msg)) + return + def save(self, fp): if fp.tell() == 0: # skip TIFF header on subsequent pages diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f5df2c301..7ec6d9e60 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -99,9 +99,11 @@ class TestFileTiff(PillowTestCase): lambda: TiffImagePlugin.TiffImageFile(invalid_file)) def test_bad_exif(self): - image = Image.open('Tests/images/hopper_bad_exif.jpg') - image._getexif() - self.assertRaises(Exception, image._getexif) + try: + Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() + except struct.error: + self.fail( + "Bad EXIF data passed incorrect values to _binary unpack") def test_save_unsupported_mode(self): im = hopper("HSV") From 426c9d8fc2b9407f4df8fb8ce27acca96333a4ca Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 11 Sep 2015 14:24:35 -0700 Subject: [PATCH 15/37] test failing update --- Tests/test_file_tiff_metadata.py | 99 +++++++++++++++----------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index df1934482..449c67517 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -34,64 +34,59 @@ class TestFileTiffMetadata(PillowTestCase): img.save(f, tiffinfo=info) - for legacy_api in [False, True]: - loaded = Image.open(f) - loaded.tag.legacy_api = legacy_api + loaded = Image.open(f) - self.assertEqual(loaded.tag[50838], - (len(basetextdata + " ?"),) if legacy_api else len(basetextdata + " ?")) - self.assertEqual(loaded.tag[50839], basetextdata + " ?") - loaded_float = loaded.tag[tag_ids['RollAngle']] - if legacy_api: - loaded_float = loaded_float[0] - self.assertAlmostEqual(loaded_float, floatdata, places=5) - loaded_double = loaded.tag[tag_ids['YawAngle']] - if legacy_api: - loaded_double = loaded_double[0] - self.assertAlmostEqual(loaded_double, doubledata) + self.assertEqual(loaded.tag[50838], (len(basetextdata + " ?"),)) + self.assertEqual(loaded.tag_v2[50838], len(basetextdata + " ?")) + + self.assertEqual(loaded.tag[50839], basetextdata + " ?") + self.assertEqual(loaded.tag_v2[50839], basetextdata + " ?") + + + loaded_float = loaded.tag[tag_ids['RollAngle']][0] + self.assertAlmostEqual(loaded_float, floatdata, places=5) + loaded_double = loaded.tag[tag_ids['YawAngle']][0] + self.assertAlmostEqual(loaded_double, doubledata) def test_read_metadata(self): - for legacy_api in [False, True]: - img = Image.open('Tests/images/hopper_g4.tif') - img.tag.legacy_api = legacy_api + img = Image.open('Tests/images/hopper_g4.tif') - known = {'YResolution': ((4294967295, 113653537),), - 'PlanarConfiguration': (1,), - 'BitsPerSample': (1,), - 'ImageLength': (128,), - 'Compression': (4,), - 'FillOrder': (1,), - 'RowsPerStrip': (128,), - 'ResolutionUnit': (3,), - 'PhotometricInterpretation': (0,), - 'PageNumber': (0, 1), - 'XResolution': ((4294967295, 113653537),), - 'ImageWidth': (128,), - 'Orientation': (1,), - 'StripByteCounts': (1968,), - 'SamplesPerPixel': (1,), - 'StripOffsets': (8,) - } if legacy_api else { - 'YResolution': 4294967295 / 113653537, - 'PlanarConfiguration': 1, - 'BitsPerSample': (1,), - 'ImageLength': 128, - 'Compression': 4, - 'FillOrder': 1, - 'RowsPerStrip': 128, - 'ResolutionUnit': 3, - 'PhotometricInterpretation': 0, - 'PageNumber': (0, 1), - 'XResolution': 4294967295 / 113653537, - 'ImageWidth': 128, - 'Orientation': 1, - 'StripByteCounts': (1968,), - 'SamplesPerPixel': 1, - 'StripOffsets': (8,) - } + self.assertEqual({'YResolution': 4294967295 / 113653537, + 'PlanarConfiguration': 1, + 'BitsPerSample': (1,), + 'ImageLength': 128, + 'Compression': 4, + 'FillOrder': 1, + 'RowsPerStrip': 128, + 'ResolutionUnit': 3, + 'PhotometricInterpretation': 0, + 'PageNumber': (0, 1), + 'XResolution': 4294967295 / 113653537, + 'ImageWidth': 128, + 'Orientation': 1, + 'StripByteCounts': (1968,), + 'SamplesPerPixel': 1, + 'StripOffsets': (8,) + }, img.tag_v2.named()) - self.assertEqual(known, img.tag.named()) + self.assertEqual({'YResolution': ((4294967295, 113653537),), + 'PlanarConfiguration': (1,), + 'BitsPerSample': (1,), + 'ImageLength': (128,), + 'Compression': (4,), + 'FillOrder': (1,), + 'RowsPerStrip': (128,), + 'ResolutionUnit': (3,), + 'PhotometricInterpretation': (0,), + 'PageNumber': (0, 1), + 'XResolution': ((4294967295, 113653537),), + 'ImageWidth': (128,), + 'Orientation': (1,), + 'StripByteCounts': (1968,), + 'SamplesPerPixel': (1,), + 'StripOffsets': (8,) + }, img.tag.named()) def test_write_metadata(self): """ Test metadata writing through the python code """ From b1fdff4034d3f2c33ba534ed10d8fd966fd32993 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Fri, 11 Sep 2015 23:44:23 +0100 Subject: [PATCH 16/37] In a twisty maze of bytes, text and arbitrary metadata, py2 and py3. New IFD is putting textdata in type7 metadata and returning bytes, old one put it in type 2 string and returned a string. This may be an issue --- PIL/TiffImagePlugin.py | 2 +- Tests/test_file_tiff_metadata.py | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 4638acd8f..243524e0f 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -361,7 +361,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): self.tagtype[tag] = 2 if self.tagtype[tag] == 7 and bytes is not str: - values = [value.encode("ascii") if isinstance(value, str) else value + values = [value.encode("ascii",'replace') if isinstance(value, str) else value for value in values] values = tuple(info.cvt_enum(value) for value in values) if info.length == 1: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 449c67517..0a7425d7b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -18,29 +18,32 @@ class TestFileTiffMetadata(PillowTestCase): img = hopper() basetextdata = "This is some arbitrary metadata for a text field" - textdata = basetextdata + " \xff" + textdata = basetextdata + " \xff" + reloaded_textdata = basetextdata.encode('ascii') + b" ?" floatdata = 12.345 doubledata = 67.89 info = TiffImagePlugin.ImageFileDirectory() - info[tag_ids['ImageJMetaDataByteCounts']] = len(textdata) + info[tag_ids['ImageJMetaDataByteCounts']] = len(reloaded_textdata) info[tag_ids['ImageJMetaData']] = textdata info[tag_ids['RollAngle']] = floatdata info.tagtype[tag_ids['RollAngle']] = 11 info[tag_ids['YawAngle']] = doubledata info.tagtype[tag_ids['YawAngle']] = 12 + print(info.tagtype) + f = self.tempfile("temp.tif") img.save(f, tiffinfo=info) loaded = Image.open(f) - self.assertEqual(loaded.tag[50838], (len(basetextdata + " ?"),)) - self.assertEqual(loaded.tag_v2[50838], len(basetextdata + " ?")) + self.assertEqual(loaded.tag[50838], (len(reloaded_textdata),)) + self.assertEqual(loaded.tag_v2[50838], len(reloaded_textdata)) - self.assertEqual(loaded.tag[50839], basetextdata + " ?") - self.assertEqual(loaded.tag_v2[50839], basetextdata + " ?") + self.assertEqual(loaded.tag[50839], reloaded_textdata) + self.assertEqual(loaded.tag_v2[50839], reloaded_textdata) loaded_float = loaded.tag[tag_ids['RollAngle']][0] From 0c35194167846698b3f0173dc09b57aba27a327f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sat, 12 Sep 2015 10:11:10 +0100 Subject: [PATCH 17/37] rewrite of #1416 working --- Tests/test_file_tiff_metadata.py | 36 +++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 0a7425d7b..ed968ef00 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -17,34 +17,50 @@ class TestFileTiffMetadata(PillowTestCase): img = hopper() + # Behaviour change: + # Pre ifd rewrite, ImageJMetaData was being written as a string(2), + # Post ifd rewrite, it's defined as arbitrary bytes(7). It should + # roundtrip with the actual bytes, rather than stripped text + # of the premerge tests. + # + # For text items, we still have to decode('ascii','replace') because + # the tiff file format can't take 8 bit bytes in that field. + basetextdata = "This is some arbitrary metadata for a text field" - textdata = basetextdata + " \xff" - reloaded_textdata = basetextdata.encode('ascii') + b" ?" + bindata = basetextdata.encode('ascii') + b" \xff" + textdata = basetextdata + " " + chr(255) + reloaded_textdata = basetextdata + " ?" floatdata = 12.345 doubledata = 67.89 info = TiffImagePlugin.ImageFileDirectory() - info[tag_ids['ImageJMetaDataByteCounts']] = len(reloaded_textdata) - info[tag_ids['ImageJMetaData']] = textdata + ImageJMetaData = tag_ids['ImageJMetaData'] + ImageJMetaDataByteCounts = tag_ids['ImageJMetaDataByteCounts'] + ImageDescription = tag_ids['ImageDescription'] + + info[ImageJMetaDataByteCounts] = len(bindata) + info[ImageJMetaData] = bindata info[tag_ids['RollAngle']] = floatdata info.tagtype[tag_ids['RollAngle']] = 11 info[tag_ids['YawAngle']] = doubledata info.tagtype[tag_ids['YawAngle']] = 12 - print(info.tagtype) - + info[ImageDescription] = textdata + f = self.tempfile("temp.tif") img.save(f, tiffinfo=info) loaded = Image.open(f) - self.assertEqual(loaded.tag[50838], (len(reloaded_textdata),)) - self.assertEqual(loaded.tag_v2[50838], len(reloaded_textdata)) + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata)) - self.assertEqual(loaded.tag[50839], reloaded_textdata) - self.assertEqual(loaded.tag_v2[50839], reloaded_textdata) + self.assertEqual(loaded.tag[ImageJMetaData], bindata) + self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) + self.assertEqual(loaded.tag[ImageDescription], (reloaded_textdata,)) + self.assertEqual(loaded.tag_v2[ImageDescription], reloaded_textdata) loaded_float = loaded.tag[tag_ids['RollAngle']][0] self.assertAlmostEqual(loaded_float, floatdata, places=5) From 156972874da19642d92b9b328bb9c7d8782f02af Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 10:53:47 +0100 Subject: [PATCH 18/37] missed a set of _v2 versioning --- PIL/TiffImagePlugin.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 243524e0f..f8fbae224 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1085,16 +1085,13 @@ def _save(im, fp, filename): # additions written by Greg Couch, gregc@cgl.ucsf.edu # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com - if hasattr(im, 'tag'): + if hasattr(im, 'tag_v2'): # preserve tags from original TIFF image file - orig_api = im.tag.legacy_api - im.tag.legacy_api = False for key in (RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, XMP): - if key in im.tag: - ifd[key] = im.tag[key] - ifd.tagtype[key] = im.tag.tagtype.get(key, None) - im.tag.legacy_api = orig_api + if key in im.tag_v2: + ifd[key] = im.tag_v2[key] + ifd.tagtype[key] = im.tag_v2.tagtype.get(key, None) # preserve ICC profile (should also work when saving other formats # which support profiles as TIFF) -- 2008-06-06 Florian Hoech From a9396ab4120f68759a1eda00f078d5d5ba00ac4f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 11:07:43 +0100 Subject: [PATCH 19/37] convert load_* functions to pure functions with no state --- PIL/TiffImagePlugin.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index f8fbae224..248fab15f 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -319,7 +319,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): data = self._tagdata[tag] typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] - self[tag] = handler(self, data) # check type + self[tag] = handler(self, self.legacy_api, data) # check type val = self._tags[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): val = val, @@ -404,7 +404,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = size, lambda self, data: ( + _load_dispatch[idx] = size, lambda self, legacy_api, data: ( self._unpack("{0}{1}".format(len(data) // size, fmt), data)) _write_dispatch[idx] = lambda self, *values: ( b"".join(self._pack(fmt, value) for value in values)) @@ -415,8 +415,8 @@ class ImageFileDirectory_v2(collections.MutableMapping): (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, data): - return (data if self.legacy_api else + def load_byte(self, legacy_api, data): + return (data if legacy_api else tuple(map(ord, data) if bytes is str else data)) @_register_writer(1) # Basic type, except for the legacy API. @@ -424,7 +424,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return data @_register_loader(2, 1) - def load_string(self, data): + def load_string(self, legacy_api, data): if data.endswith(b"\0"): data = data[:-1] return data.decode("latin-1", "replace") @@ -437,9 +437,9 @@ class ImageFileDirectory_v2(collections.MutableMapping): return b"" + value.encode('ascii', 'replace') + b"\0" @_register_loader(5, 8) - def load_rational(self, data): + def load_rational(self, legacy_api, data): vals = self._unpack("{0}L".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if self.legacy_api else a / b + combine = lambda a, b: (a, b) if legacy_api else a / b return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -449,7 +449,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): for frac in values) @_register_loader(7, 1) - def load_undefined(self, data): + def load_undefined(self, legacy_api, data): return data @_register_writer(7) @@ -457,9 +457,9 @@ class ImageFileDirectory_v2(collections.MutableMapping): return value @_register_loader(10, 8) - def load_signed_rational(self, data): + def load_signed_rational(self, legacy_api, data): vals = self._unpack("{0}l".format(len(data) // 4), data) - combine = lambda a, b: (a, b) if self.legacy_api else a / b + combine = lambda a, b: (a, b) if legacy_api else a / b return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) From e1236d702d859f2360827823e6904d5c205a2ef0 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 14:01:01 +0100 Subject: [PATCH 20/37] v1/v2 tag storage in IFD, legacy_api as a parameter to _saveitem, save both _tags_v* when saving as legacy api --- PIL/TiffImagePlugin.py | 112 ++++++++++++++++++++++++++++++++--------- 1 file changed, 88 insertions(+), 24 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 248fab15f..e1738cee9 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -244,15 +244,26 @@ class ImageFileDirectory_v2(collections.MutableMapping): * self.tagtype = {} Key: numerical tiff tag number Value: integer corresponding to the data type from `TiffTags.TYPES` - + """ + """ + Documentation: + 'internal' - * self._tags = {} Key: numerical tiff tag number - Value: decoded data, as tuple for multiple values + * self._tags_v2 = {} Key: numerical tiff tag number + Value: decoded data, as tuple for multiple values * self._tagdata = {} Key: numerical tiff tag number Value: undecoded byte string from file - + * self._tags_v1 = {} Key: numerical tiff tag number + Value: decoded data in the v1 format + Tags will be found in the private attributes self._tagdata, and in - self._tags once decoded. + self._tags_v2 once decoded. + + If legacy_api is true, then decoded tags will be populated into both + _tags_v1 and _tags_v2. _Tags_v2 will be used if this IFD is used in + the TIFF save routine. + + Tags will be read from tags_v1 if legacy_api == true. """ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): @@ -278,19 +289,18 @@ class ImageFileDirectory_v2(collections.MutableMapping): self.reset() self.next, = self._unpack("L", ifh[4:]) self._legacy_api = False - + prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) legacy_api = property(lambda self: self._legacy_api) @legacy_api.setter def legacy_api(self, value): - if value != self._legacy_api: - self._tags.clear() - self._legacy_api = value + raise Exception("Not allowing setting of legacy api") def reset(self): - self._tags = {} + self._tags_v1 = {} # will remain empty if legacy_api is false + self._tags_v2 = {} # main tag storage self._tagdata = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None @@ -312,27 +322,30 @@ class ImageFileDirectory_v2(collections.MutableMapping): for code, value in self.items()) def __len__(self): - return len(set(self._tagdata) | set(self._tags)) + return len(set(self._tagdata) | set(self._tags_v2)) def __getitem__(self, tag): - if tag not in self._tags: # unpack on the fly + if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] self[tag] = handler(self, self.legacy_api, data) # check type - val = self._tags[tag] + val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): val = val, return val def __contains__(self, tag): - return tag in self._tags or tag in self._tagdata + return tag in self._tags_v2 or tag in self._tagdata if bytes is str: def has_key(self, tag): return tag in self def __setitem__(self, tag, value): + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag, value, legacy_api): basetypes = (Number, bytes, str) if bytes is str: basetypes += unicode, @@ -363,20 +376,25 @@ class ImageFileDirectory_v2(collections.MutableMapping): if self.tagtype[tag] == 7 and bytes is not str: values = [value.encode("ascii",'replace') if isinstance(value, str) else value for value in values] + values = tuple(info.cvt_enum(value) for value in values) + + dest = self._tags_v1 if legacy_api else self._tags_v2 + if info.length == 1: - if self.legacy_api and self.tagtype[tag] in [5, 10]: + if legacy_api and self.tagtype[tag] in [5, 10]: values = values, - self._tags[tag], = values + dest[tag], = values else: - self._tags[tag] = values + dest[tag] = values def __delitem__(self, tag): - self._tags.pop(tag, None) + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) def __iter__(self): - return iter(set(self._tagdata) | set(self._tags)) + return iter(set(self._tagdata) | set(self._tags_v2)) def _unpack(self, fmt, data): return struct.unpack(self._endian + fmt, data) @@ -536,15 +554,15 @@ class ImageFileDirectory_v2(collections.MutableMapping): fp.write(self._prefix + self._pack("HL", 42, 8)) # FIXME What about tagdata? - fp.write(self._pack("H", len(self._tags))) + fp.write(self._pack("H", len(self._tags_v2))) entries = [] - offset = fp.tell() + len(self._tags) * 12 + 4 + offset = fp.tell() + len(self._tags_v2) * 12 + 4 stripoffsets = None # pass 1: convert tags to binary format # always write tags in ascending order - for tag, value in sorted(self._tags.items()): + for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) typ = self.tagtype.get(tag) @@ -609,18 +627,62 @@ del _load_dispatch, _write_dispatch, idx, name class ImageFileDirectory_v1(ImageFileDirectory_v2): def __init__(self, *args, **kwargs): ImageFileDirectory_v2.__init__(self, *args, **kwargs) - self.legacy_api=True + self._legacy_api=True #insert deprecation warning here. - tags = property(lambda self: self._tags) + tags = property(lambda self: self._tags_v1) tagdata = property(lambda self: self._tagdata) @classmethod def from_v2(cls, original): + """ returns: ImageFileDirectory_v1 + + Returns an ImageFileDirectory_v1 instance with the same + data as is contained in the original ImageFileDirectory_v2 + instance """ + ifd = cls(prefix=original.prefix) ifd._tagdata = original._tagdata ifd.tagtype = original.tagtype return ifd + + def to_v2(self): + """ returns: ImageFileDirectory_v2 + + Returns an ImageFileDirectory_v2 instance with the same + data as is contained in this ImageFileDirectory_v1 instance """ + + ifd = ImageFileDirectory_v2(prefix=self.prefix) + ifd._tagdata = dict(self._tagdata) + ifd.tagtype = dict(self.tagtype) + ifd._tags_v2 = dict(self._tags_v2) + return ifd + + def __contains__(self, tag): + return tag in self._tags_v1 or tag in self._tagdata + + def __len__(self): + return len(set(self._tagdata) | set(self._tags_v1)) + + def __iter__(self): + return iter(set(self._tagdata) | set(self._tags_v1)) + + def __setitem__(self, tag, value): + for legacy_api in (False,True): + self._setitem(tag, value, legacy_api) + + def __getitem__(self, tag): + if tag not in self._tags_v1: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + for legacy in (False, True): + self._setitem(tag, handler(self, legacy, data), legacy) + val = self._tags_v1[tag] + if not isinstance(val, (tuple, bytes)): + val = val, + return val + # undone -- switch this pointer when IFD_LEGACY_API == False ImageFileDirectory = ImageFileDirectory_v1 @@ -1076,6 +1138,8 @@ def _save(im, fp, filename): info = im.encoderinfo.get("tiffinfo", {}) if DEBUG: print("Tiffinfo Keys: %s" % list(info)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() for key in info: ifd[key] = info.get(key) try: From 26366798686049656f68509ac6fbc93d49cd4324 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 14:08:49 +0100 Subject: [PATCH 21/37] updating tests for legacy_api api change --- Tests/test_file_libtiff.py | 13 +++++++++---- Tests/test_file_tiff.py | 9 ++++----- Tests/test_file_tiff_metadata.py | 6 +++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 33d77f558..5d4016555 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -127,11 +127,14 @@ class TestFileLibTiff(LibTiffTestCase): """ Test metadata writing through libtiff """ for legacy_api in [False, True]: img = Image.open('Tests/images/hopper_g4.tif') - img.tag.legacy_api = legacy_api f = self.tempfile('temp.tiff') img.save(f, tiffinfo=img.tag) - original = img.tag.named() + + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() # PhotometricInterpretation is set from SAVE_INFO, # not the original image. @@ -139,8 +142,10 @@ class TestFileLibTiff(LibTiffTestCase): 'PhotometricInterpretation'] loaded = Image.open(f) - loaded.tag.legacy_api = legacy_api - reloaded = loaded.tag.named() + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() for tag, value in itertools.chain(reloaded.items(), original.items()): diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 7ec6d9e60..2d787e66d 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -254,27 +254,26 @@ class TestFileTiff(PillowTestCase): def test_load_byte(self): for legacy_api in [False, True]: ifd = TiffImagePlugin.ImageFileDirectory_v2() - ifd.legacy_api = legacy_api data = b"abc" - ret = ifd.load_byte(data) + ret = ifd.load_byte(legacy_api, data) self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" - ret = ifd.load_string(data) + ret = ifd.load_string(False, data) self.assertEqual(ret, "abc") def test_load_float(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" - ret = ifd.load_float(data) + ret = ifd.load_float(False, data) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" - ret = ifd.load_double(data) + ret = ifd.load_double(False, data) self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) def test_seek(self): diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index ed968ef00..05aa98bb5 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -17,7 +17,7 @@ class TestFileTiffMetadata(PillowTestCase): img = hopper() - # Behaviour change: + # Behaviour change: re #1416 # Pre ifd rewrite, ImageJMetaData was being written as a string(2), # Post ifd rewrite, it's defined as arbitrary bytes(7). It should # roundtrip with the actual bytes, rather than stripped text @@ -116,8 +116,8 @@ class TestFileTiffMetadata(PillowTestCase): loaded = Image.open(f) - original = img.tag.named() - reloaded = loaded.tag.named() + original = img.tag_v2.named() + reloaded = loaded.tag_v2.named() ignored = [ 'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] From bb75b2d6adec0c3dbf9a6239d49a0d46d9947ac1 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 14:09:42 +0100 Subject: [PATCH 22/37] Added doc comment --- PIL/TiffTags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 6cc173332..3e7aa3613 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -31,7 +31,9 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): ## # Map tag numbers to tag info. - +# +# id: (Name, Type, Length, enum_values) +# TAGS = { 254: ("NewSubfileType", 4, 1), From ca24a441006b1c613617b4035848bfcd23177518 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 14:16:12 +0100 Subject: [PATCH 23/37] Rewrap intelligently --- PIL/TiffImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index e1738cee9..0044c567e 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1123,8 +1123,8 @@ def _save(im, fp, filename): ifd = ImageFileDirectory_v2(prefix=prefix) - compression = im.encoderinfo.get('compression', im.info.get('compression', - 'raw')) + compression = im.encoderinfo.get('compression', + im.info.get('compression', 'raw')) libtiff = WRITE_LIBTIFF or compression != 'raw' From f3ab9b9f812402c0ac32bdd6135a093425435e22 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 14:38:51 +0100 Subject: [PATCH 24/37] temp removing segfaulting test on travis --- Tests/test_file_libtiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 5d4016555..0a64b0f8b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -198,7 +198,7 @@ class TestFileLibTiff(LibTiffTestCase): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def test_big_endian(self): + def xtest_big_endian(self): im = Image.open('Tests/images/16bit.MM.deflate.tif') self.assertEqual(im.getpixel((0, 0)), 480) From 4596df45c18d3f5fe4088bfbf8bf496406a24f8d Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 15:15:13 +0100 Subject: [PATCH 25/37] Versioned interface for TiffTags --- PIL/TiffImagePlugin.py | 10 +++++----- PIL/TiffTags.py | 20 ++++++++++++++------ Tests/test_file_tiff_metadata.py | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 0044c567e..648aa68f0 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -55,7 +55,7 @@ import struct import sys import warnings -from .TiffTags import TAGS, TYPES, TagInfo +from .TiffTags import TAGS_V2, TYPES, TagInfo __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -318,7 +318,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): """ Returns the complete tag dictionary, with named tags where possible. """ - return dict((TAGS.get(code, TagInfo()).name, value) + return dict((TAGS_V2.get(code, TagInfo()).name, value) for code, value in self.items()) def __len__(self): @@ -350,7 +350,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): if bytes is str: basetypes += unicode, - info = TAGS.get(tag, TagInfo()) + info = TAGS_V2.get(tag, TagInfo()) values = [value] if isinstance(value, basetypes) else value if tag not in self.tagtype: @@ -503,7 +503,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): for i in range(self._unpack("H", self._ensure_read(fp,2))[0]): tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp,12)) if DEBUG: - tagname = TAGS.get(tag, TagInfo()).name + tagname = TAGS_V2.get(tag, TagInfo()).name typname = TYPES.get(typ, "unknown") print("tag: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), end=" ") @@ -571,7 +571,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): values = value if isinstance(value, tuple) else (value,) data = self._write_dispatch[typ](self, *values) if DEBUG: - tagname = TAGS.get(tag, TagInfo()).name + tagname = TAGS_V2.get(tag, TagInfo()).name typname = TYPES.get(typ, "unknown") print("save: %s (%d) - type: %s (%d)" % (tagname, tag, typname, typ), end=" ") diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 3e7aa3613..8fb851cda 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -19,6 +19,9 @@ from collections import namedtuple +# Legacy Tags structure +TAGS = {} + class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] @@ -34,7 +37,7 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): # # id: (Name, Type, Length, enum_values) # -TAGS = { +TAGS_V2 = { 254: ("NewSubfileType", 4, 1), 255: ("SubfileType", 3, 1), @@ -160,12 +163,17 @@ TAGS = { 50839: ("ImageJMetaData", 7, 1) } +def _populate(): + for k, v in TAGS_V2.items(): + # Populate legacy structure. + TAGS[k] = v[0] + if len(v) == 4: + for sk,sv in v[3].items(): + TAGS[(k,sk)] = sv + + TAGS_V2[k] = TagInfo(k, *v) -for k, v in TAGS.items(): - TAGS[k] = TagInfo(k, *v) -del k, v - - +_populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 05aa98bb5..1b5e05b4f 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -4,7 +4,7 @@ from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags -tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS.values()) +tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values()) class TestFileTiffMetadata(PillowTestCase): From 70977bcbb5e3ce970da8a43f218bdbcef441c0cd Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 15:45:15 +0100 Subject: [PATCH 26/37] Got the order of the enums wrong --- PIL/TiffTags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 8fb851cda..ddc7cde6d 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -157,7 +157,7 @@ TAGS_V2 = { 45580: ("PitchAngle", 10, 1), 45581: ("RollAngle", 10, 1), - 50741: ("MakerNoteSafety", 3, 1, {0: "Unsafe", 1: "Safe"}), + 50741: ("MakerNoteSafety", 3, 1, {"Unsafe": 0, "Safe": 1}), 50780: ("BestQualityScale", 5, 1), 50838: ("ImageJMetaDataByteCounts", 4, 1), 50839: ("ImageJMetaData", 7, 1) @@ -168,8 +168,8 @@ def _populate(): # Populate legacy structure. TAGS[k] = v[0] if len(v) == 4: - for sk,sv in v[3].items(): - TAGS[(k,sk)] = sv + for sk, sv in v[3].items(): + TAGS[(k, sv)] = sk TAGS_V2[k] = TagInfo(k, *v) From 86bda9a705bef90e72427d0069eece86b61175f5 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 15:55:17 +0100 Subject: [PATCH 27/37] Legacy tifftags --- PIL/TiffTags.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 4 deletions(-) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index ddc7cde6d..4f3352f31 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -19,9 +19,6 @@ from collections import namedtuple -# Legacy Tags structure -TAGS = {} - class TagInfo(namedtuple("_TagInfo", "value name type length enum")): __slots__ = [] @@ -163,6 +160,127 @@ TAGS_V2 = { 50839: ("ImageJMetaData", 7, 1) } +# Legacy Tags structure +# these tags aren't included above, but were in the previous versions +TAGS = {347: 'JPEGTables', + 700: 'XMP', + + # Additional Exif Info + 33434: 'ExposureTime', + 33437: 'FNumber', + 33723: 'IptcNaaInfo', + 34377: 'PhotoshopInfo', + 34675: 'ICCProfile', + 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', + + # 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', + 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', + 50778: 'CalibrationIlluminant1', + 50779: 'CalibrationIlluminant2', +} + + def _populate(): for k, v in TAGS_V2.items(): # Populate legacy structure. @@ -170,7 +288,7 @@ def _populate(): if len(v) == 4: for sk, sv in v[3].items(): TAGS[(k, sv)] = sk - + TAGS_V2[k] = TagInfo(k, *v) _populate() From c2818ee09e2f858d3c50d7a2ec30256a4bab01e2 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 16:16:26 +0100 Subject: [PATCH 28/37] Add versioned api to tests --- Tests/test_file_libtiff.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 0a64b0f8b..57eeec43d 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -232,7 +232,8 @@ class TestFileLibTiff(LibTiffTestCase): orig.save(out) reread = Image.open(out) - self.assertEqual('temp.tif', reread.tag[269]) + self.assertEqual('temp.tif', reread.tag_v2[269]) + self.assertEqual('temp.tif', reread.tag[269][0]) def test_12bit_rawmode(self): """ Are we generating the same interpretation From b56d5ca40323671a500b114a42f5059bcc15e0d3 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 13 Sep 2015 16:16:50 +0100 Subject: [PATCH 29/37] Added indicator for multipage tiffs --- PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 648aa68f0..e3ea03ab6 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -644,6 +644,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ifd = cls(prefix=original.prefix) ifd._tagdata = original._tagdata ifd.tagtype = original.tagtype + ifd.next = original.next # an indicator for multipage tiffs return ifd def to_v2(self): From 05348d4f8fd75b2e6727a39afdd2da729482875e Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Sep 2015 03:03:24 -0700 Subject: [PATCH 30/37] Reenabling failing/crashing tests, with fixes --- PIL/TiffImagePlugin.py | 6 +++++- Tests/test_file_libtiff.py | 2 +- Tests/test_file_tiff.py | 5 +---- encode.c | 6 +++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index e3ea03ab6..fcc84607c 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -1230,8 +1230,12 @@ def _save(im, fp, filename): # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. + legacy_ifd = {} + if hasattr(im, 'tag'): + legacy_ifd = im.tag.to_v2() for k, v in itertools.chain(ifd.items(), - getattr(im, 'ifd', {}).items()): + getattr(im, 'tag_v2', {}).items(), + legacy_ifd.items()): if k not in atts and k not in blocklist: if isinstance(v, unicode if bytes is str else str): atts[k] = v.encode('ascii', 'replace') + b"\0" diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 57eeec43d..f900b97cf 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -198,7 +198,7 @@ class TestFileLibTiff(LibTiffTestCase): # UNDONE - libtiff defaults to writing in native endian, so # on big endian, we'll get back mode = 'I;16B' here. - def xtest_big_endian(self): + def test_big_endian(self): im = Image.open('Tests/images/16bit.MM.deflate.tif') self.assertEqual(im.getpixel((0, 0)), 480) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 2d787e66d..c21eba6f4 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -308,10 +308,7 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "L") self.assert_image_similar(im, original, 7.3) -### -# UNDONE -### Segfaulting - def xtest_page_number_x_0(self): + def test_page_number_x_0(self): # Issue 973 # Test TIFF with tag 297 (Page Number) having value of 0 0. # The first number is the current page number. diff --git a/encode.c b/encode.c index 244f5ca0d..c46d78426 100644 --- a/encode.c +++ b/encode.c @@ -775,7 +775,7 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) len, intav); free(intav); } - } else { + } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { TRACE((" %d elements, setting as floats \n", len)); floatav = malloc(sizeof(float)*len); if (floatav) { @@ -787,6 +787,10 @@ PyImaging_LibTiffEncoderNew(PyObject* self, PyObject* args) len, floatav); free(floatav); } + } else { + TRACE(("Unhandled type in tuple for key %d : %s \n", + (int)PyInt_AsLong(key), + PyBytes_AsString(PyObject_Str(value)))); } } } else { From 4adbc9735c7528638550f2a9e591f33d6222b38b Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Sep 2015 04:35:09 -0700 Subject: [PATCH 31/37] Reorder load_* parameters to pass master tests --- PIL/TiffImagePlugin.py | 16 ++++++++-------- Tests/test_file_tiff.py | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index fcc84607c..d1acc3496 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -329,7 +329,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): data = self._tagdata[tag] typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] - self[tag] = handler(self, self.legacy_api, data) # check type + self[tag] = handler(self, data, self.legacy_api) # check type val = self._tags_v2[tag] if self.legacy_api and not isinstance(val, (tuple, bytes)): val = val, @@ -422,7 +422,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = size, lambda self, legacy_api, data: ( + _load_dispatch[idx] = size, lambda self, data, legacy_api=True: ( self._unpack("{0}{1}".format(len(data) // size, fmt), data)) _write_dispatch[idx] = lambda self, *values: ( b"".join(self._pack(fmt, value) for value in values)) @@ -433,7 +433,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): (9, "l", "signed long"), (11, "f", "float"), (12, "d", "double")])) @_register_loader(1, 1) # Basic type, except for the legacy API. - def load_byte(self, legacy_api, data): + def load_byte(self, data, legacy_api=True): return (data if legacy_api else tuple(map(ord, data) if bytes is str else data)) @@ -442,7 +442,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return data @_register_loader(2, 1) - def load_string(self, legacy_api, data): + def load_string(self, data, legacy_api=True): if data.endswith(b"\0"): data = data[:-1] return data.decode("latin-1", "replace") @@ -455,7 +455,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return b"" + value.encode('ascii', 'replace') + b"\0" @_register_loader(5, 8) - def load_rational(self, legacy_api, data): + def load_rational(self, data, legacy_api=True): vals = self._unpack("{0}L".format(len(data) // 4), data) combine = lambda a, b: (a, b) if legacy_api else a / b return tuple(combine(num, denom) @@ -467,7 +467,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): for frac in values) @_register_loader(7, 1) - def load_undefined(self, legacy_api, data): + def load_undefined(self, data, legacy_api=True): return data @_register_writer(7) @@ -475,7 +475,7 @@ class ImageFileDirectory_v2(collections.MutableMapping): return value @_register_loader(10, 8) - def load_signed_rational(self, legacy_api, data): + def load_signed_rational(self, data, legacy_api=True): vals = self._unpack("{0}l".format(len(data) // 4), data) combine = lambda a, b: (a, b) if legacy_api else a / b return tuple(combine(num, denom) @@ -678,7 +678,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): typ = self.tagtype[tag] size, handler = self._load_dispatch[typ] for legacy in (False, True): - self._setitem(tag, handler(self, legacy, data), legacy) + self._setitem(tag, handler(self, data, legacy), legacy) val = self._tags_v1[tag] if not isinstance(val, (tuple, bytes)): val = val, diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index c21eba6f4..57adcdd18 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -255,25 +255,25 @@ class TestFileTiff(PillowTestCase): for legacy_api in [False, True]: ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc" - ret = ifd.load_byte(legacy_api, data) + ret = ifd.load_byte(data, legacy_api) self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" - ret = ifd.load_string(False, data) + ret = ifd.load_string(data, False) self.assertEqual(ret, "abc") def test_load_float(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" - ret = ifd.load_float(False, data) + ret = ifd.load_float(data, False) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" - ret = ifd.load_double(False, data) + ret = ifd.load_double(data, False) self.assertEqual(ret, (8.540883223036124e+194, 8.540883223036124e+194)) def test_seek(self): From 9286c9e460c241cc45c479678c4bf45a5116296f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Sep 2015 05:10:27 -0700 Subject: [PATCH 32/37] Reenabling and fixing the former test_xyres_tiff test for integer resolutions --- Tests/test_file_tiff.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 57adcdd18..f8ce199da 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -85,12 +85,16 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.info['dpi'], (72., 72.)) - def xtest_int_resolution(self): + def test_int_resolution(self): + from PIL.TiffImagePlugin import X_RESOLUTION, Y_RESOLUTION + filename = "Tests/images/pil168.tif" + im = Image.open(filename) + # Try to read a file where X,Y_RESOLUTION are ints - im.tag[X_RESOLUTION] = (72,) - im.tag[Y_RESOLUTION] = (72,) + im.tag_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 im._setup() - self.assertEqual(im.info['dpi'], (72., 72.)) + self.assertEqual(im.info['dpi'], (71., 71.)) def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" From 1614f2fdb56465308d09d2b516c1c059b8cc5bc1 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Sep 2015 07:01:57 -0700 Subject: [PATCH 33/37] Documentation for IFD Changes --- PIL/TiffImagePlugin.py | 97 +++++++++++++++++++++------- PIL/TiffTags.py | 17 +++++ docs/PIL.rst | 8 --- docs/handbook/image-file-formats.rst | 42 +++++++++--- docs/reference/index.rst | 1 + 5 files changed, 124 insertions(+), 41 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index d1acc3496..db0427267 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -231,24 +231,36 @@ class ImageFileDirectory_v2(collections.MutableMapping): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. - 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] + ifd = ImageFileDirectory_v2() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print(ifd[key]) + 'Some Data' + + Individual values are returned as the strings or numbers, sequences are + returned as tuples of the values. - Also contains a dictionary of tag types as read from the tiff image file, - 'ImageFileDirectory.tagtype' + The tiff metadata type of each item is stored in a dictionary of + tag types in + `~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types + are read from a tiff file, guessed from the type added, or added + manually. Data Structures: - 'public' - * self.tagtype = {} Key: numerical tiff tag number - Value: integer corresponding to the data type from - `TiffTags.TYPES` + + * self.tagtype = {} + + * Key: numerical tiff tag number + * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` + + .. versionadded:: 3.0.0 """ """ Documentation: - 'internal' + 'internal' data structures: * self._tags_v2 = {} Key: numerical tiff tag number Value: decoded data, as tuple for multiple values * self._tagdata = {} Key: numerical tiff tag number @@ -259,11 +271,13 @@ class ImageFileDirectory_v2(collections.MutableMapping): Tags will be found in the private attributes self._tagdata, and in self._tags_v2 once decoded. - If legacy_api is true, then decoded tags will be populated into both - _tags_v1 and _tags_v2. _Tags_v2 will be used if this IFD is used in - the TIFF save routine. - - Tags will be read from tags_v1 if legacy_api == true. + Self.legacy_api is a value for internal use, and shouldn't be + changed from outside code. In cooperation with the + ImageFileDirectory_v1 class, if legacy_api is true, then decoded + tags will be populated into both _tags_v1 and _tags_v2. _Tags_v2 + will be used if this IFD is used in the TIFF save routine. Tags + should be read from tags_v1 if legacy_api == true. + """ def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None): @@ -273,9 +287,9 @@ class ImageFileDirectory_v2(collections.MutableMapping): magic header to the constructor. To only set the endianness, pass it as the 'prefix' keyword argument. - :ifh: One of the accepted magic headers (cf. PREFIXES); also sets + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets endianness. - :prefix: Override the endianness of the file. + :param prefix: Override the endianness of the file. """ if ifh[:4] not in PREFIXES: raise SyntaxError("not a TIFF file (header %r not valid)" % ifh) @@ -310,12 +324,19 @@ class ImageFileDirectory_v2(collections.MutableMapping): return str(dict(self)) def as_dict(self): - """Return a dictionary of the image's tags.""" + """Return a dictionary of the image's tags. + + use `dict(ifd)` instead. + + .. deprecated:: 3.0.0 + """ # FIXME Deprecate: use dict(self) return dict(self) def named(self): """ + :returns: dict of name|key: value + Returns the complete tag dictionary, with named tags where possible. """ return dict((TAGS_V2.get(code, TagInfo()).name, value) @@ -625,6 +646,23 @@ del _load_dispatch, _write_dispatch, idx, name #Legacy ImageFileDirectory support. class ImageFileDirectory_v1(ImageFileDirectory_v2): + """This class represents the **legacy** interface to a TIFF tag directory. + + Exposes a dictionary interface of the tags in the directory:: + + ifd = ImageFileDirectory_v1() + ifd[key] = 'Some Data' + ifd.tagtype[key] = 2 + print ifd[key] + ('Some Data',) + + Also contains a dictionary of tag types as read from the tiff image file, + `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype'. + + Values are returned as a tuple. + + .. deprecated:: 3.0.0 + """ def __init__(self, *args, **kwargs): ImageFileDirectory_v2.__init__(self, *args, **kwargs) self._legacy_api=True @@ -635,11 +673,15 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): @classmethod def from_v2(cls, original): - """ returns: ImageFileDirectory_v1 + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. - Returns an ImageFileDirectory_v1 instance with the same - data as is contained in the original ImageFileDirectory_v2 - instance """ + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ ifd = cls(prefix=original.prefix) ifd._tagdata = original._tagdata @@ -648,10 +690,15 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): return ifd def to_v2(self): - """ returns: ImageFileDirectory_v2 + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance. - Returns an ImageFileDirectory_v2 instance with the same - data as is contained in this ImageFileDirectory_v1 instance """ + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ ifd = ImageFileDirectory_v2(prefix=self.prefix) ifd._tagdata = dict(self._tagdata) diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index 4f3352f31..77a067981 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -296,3 +296,20 @@ _populate() # Map type numbers to type names -- defined in ImageFileDirectory. TYPES = {} + +# was: +# 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", +# } + diff --git a/docs/PIL.rst b/docs/PIL.rst index 53a61872b..32d90232e 100644 --- a/docs/PIL.rst +++ b/docs/PIL.rst @@ -144,14 +144,6 @@ can be found here. :undoc-members: :show-inheritance: -:mod:`TiffTags` Module ----------------------- - -.. automodule:: PIL.TiffTags - :members: - :undoc-members: - :show-inheritance: - :mod:`WalImageFile` Module -------------------------- diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 11ec60401..1453046ef 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -438,17 +438,37 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following **compression** Compression mode. + .. versionadded:: 2.0.0 + **dpi** - Image resolution as an (xdpi, ydpi) tuple, where applicable. You can use + Image resolution as an ``(xdpi, ydpi)`` tuple, where applicable. You can use the :py:attr:`~PIL.Image.Image.tag` attribute to get more detailed information about the image resolution. .. versionadded:: 1.1.5 -In addition, the :py:attr:`~PIL.Image.Image.tag` attribute contains a -dictionary of decoded TIFF fields. Values are stored as either strings or -tuples. Note that only short, long and ASCII tags are correctly unpacked by -this release. +**resolution** + Image resolution as an ``(xres, yres)`` tuple, where applicable. This is a + measurement in whichever unit is specified by the file. + + .. versionadded:: 1.1.5 + + +The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary of +TIFF metadata. The keys are numerical indexes from `~PIL.TiffTags.TAGS_V2`. +Values are strings or numbers for single items, multiple values are returned +in a tuple of values. Rational numbers are returned as a single value. + + .. versionadded:: 3.0.0 + +For compatibility with legacy code, the +:py:attr:`~PIL.Image.Image.tag` attribute contains a dictionary of +decoded TIFF fields as returned prior to version 3.0.0. Values are +returned as either strings or tuples of numeric values. Rational +numbers are returned as a tuple of ``(numerator, denominator)``. + + .. deprecated:: 3.0.0 + Saving Tiff Images ~~~~~~~~~~~~~~~~~~ @@ -456,17 +476,23 @@ Saving Tiff Images The :py:meth:`~PIL.Image.Image.save` method can take the following keyword arguments: **tiffinfo** - A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` object or dict + A :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object or dict object containing tiff tags and values. The TIFF field type is autodetected for Numeric and string values, any other types - require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory` + require using an :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` object and setting the type in - :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory.tagtype` with + :py:attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype` with the appropriate numerical value from ``TiffTags.TYPES``. .. versionadded:: 2.3.0 + For compatibility with legacy code, a + `~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may be passed + in this field. This will be deprecated in a future version. + + ..versionadded:: 3.0.0 + **compression** A string containing the desired compression method for the file. (valid only with libtiff installed) Valid compression diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 73a3ecfed..2d89d2100 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -26,6 +26,7 @@ Reference ImageTk ImageWin ExifTags + TiffTags OleFileIO PSDraw PixelAccess From e4f9b697c6f336f9de23ab7ef94fc88630d3f3a0 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 14 Sep 2015 07:33:07 -0700 Subject: [PATCH 34/37] Doc syntax error --- PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index db0427267..17426d4e2 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -657,7 +657,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): ('Some Data',) Also contains a dictionary of tag types as read from the tiff image file, - `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype'. + `~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`. Values are returned as a tuple. From 63f5f688373a0b5f276e0a5ed2be27cf16804f3d Mon Sep 17 00:00:00 2001 From: homm Date: Tue, 15 Sep 2015 01:15:54 +0300 Subject: [PATCH 35/37] unused imports --- PIL/IptcImagePlugin.py | 4 ++-- PIL/PixarImagePlugin.py | 1 - PIL/SgiImagePlugin.py | 1 - PIL/SunImagePlugin.py | 1 - PIL/TgaImagePlugin.py | 1 - 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/PIL/IptcImagePlugin.py b/PIL/IptcImagePlugin.py index 323177039..b5aa84bad 100644 --- a/PIL/IptcImagePlugin.py +++ b/PIL/IptcImagePlugin.py @@ -217,7 +217,7 @@ def getiptcinfo(im): while app[offset:offset+4] == b"8BIM": offset += 4 # resource code - code = JpegImagePlugin.i16(app, offset) + code = i16(app, offset) offset += 2 # resource name (usually empty) name_len = i8(app[offset]) @@ -226,7 +226,7 @@ def getiptcinfo(im): if offset & 1: offset += 1 # resource data block - size = JpegImagePlugin.i32(app, offset) + size = i32(app, offset) offset += 4 if code == 0x0404: # 0x0404 contains IPTC/NAA data diff --git a/PIL/PixarImagePlugin.py b/PIL/PixarImagePlugin.py index 26b872893..7fef35408 100644 --- a/PIL/PixarImagePlugin.py +++ b/PIL/PixarImagePlugin.py @@ -27,7 +27,6 @@ __version__ = "0.1" # helpers i16 = _binary.i16le -i32 = _binary.i32le ## diff --git a/PIL/SgiImagePlugin.py b/PIL/SgiImagePlugin.py index e73cf1601..f890c7ef6 100644 --- a/PIL/SgiImagePlugin.py +++ b/PIL/SgiImagePlugin.py @@ -24,7 +24,6 @@ __version__ = "0.2" i8 = _binary.i8 i16 = _binary.i16be -i32 = _binary.i32be def _accept(prefix): diff --git a/PIL/SunImagePlugin.py b/PIL/SunImagePlugin.py index 22f27a1c0..af63144f2 100644 --- a/PIL/SunImagePlugin.py +++ b/PIL/SunImagePlugin.py @@ -21,7 +21,6 @@ from PIL import Image, ImageFile, ImagePalette, _binary __version__ = "0.3" -i16 = _binary.i16be i32 = _binary.i32be diff --git a/PIL/TgaImagePlugin.py b/PIL/TgaImagePlugin.py index 8766e3890..a75ce2986 100644 --- a/PIL/TgaImagePlugin.py +++ b/PIL/TgaImagePlugin.py @@ -28,7 +28,6 @@ __version__ = "0.3" i8 = _binary.i8 i16 = _binary.i16le -i32 = _binary.i32le MODES = { From 9930b05a331d70ad40fb12dafbe0e5cfd03c2bfc Mon Sep 17 00:00:00 2001 From: homm Date: Tue, 15 Sep 2015 04:06:51 +0300 Subject: [PATCH 36/37] fix tiff exif loading in case when file is empty or ended --- PIL/JpegImagePlugin.py | 2 +- Tests/test_file_tiff_metadata.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/PIL/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index 3d01c5fc1..550276c02 100644 --- a/PIL/JpegImagePlugin.py +++ b/PIL/JpegImagePlugin.py @@ -428,7 +428,7 @@ def _getexif(self): # exif field 0x8825 is an offset pointer to the location # of the nested embedded gps exif ifd. # It should be a long, but may be corrupted. - file.seek(exif[0x8825]) + file.seek(exif[0x8825]) except (KeyError, TypeError): pass else: diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1b5e05b4f..f2197ad04 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,5 +1,8 @@ from __future__ import division +import io +import struct + from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags @@ -136,6 +139,15 @@ class TestFileTiffMetadata(PillowTestCase): self.assertEqual(tag_ids['MakerNoteSafety'], 50741) self.assertEqual(tag_ids['BestQualityScale'], 50780) + def test_empty_metadata(self): + f = io.BytesIO(b'II*\x00\x08\x00\x00\x00') + head = f.read(8) + info = TiffImagePlugin.ImageFileDirectory(head) + try: + self.assert_warning(UserWarning, lambda: info.load(f)) + except struct.error: + self.fail("Should not be struct errors there.") + if __name__ == '__main__': unittest.main() From 0f87b1f125d6447e020c7c498c77becd0370fb46 Mon Sep 17 00:00:00 2001 From: homm Date: Tue, 15 Sep 2015 02:52:02 +0300 Subject: [PATCH 37/37] suppress and check warning during tests --- Tests/test_file_jpeg.py | 3 ++- Tests/test_file_tiff.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e6da7bb8b..367e57c14 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -370,7 +370,8 @@ class TestFileJpeg(PillowTestCase): # Act # Shouldn't raise error - im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg") + fn = "Tests/images/sugarshack_bad_mpo_header.jpg" + im = self.assert_warning(UserWarning, lambda: Image.open(fn)) # Assert self.assertEqual(im.format, "JPEG") diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index f8ce199da..a221f15cc 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -103,8 +103,9 @@ class TestFileTiff(PillowTestCase): lambda: TiffImagePlugin.TiffImageFile(invalid_file)) def test_bad_exif(self): + i = Image.open('Tests/images/hopper_bad_exif.jpg') try: - Image.open('Tests/images/hopper_bad_exif.jpg')._getexif() + self.assert_warning(UserWarning, lambda: i._getexif()) except struct.error: self.fail( "Bad EXIF data passed incorrect values to _binary unpack")