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/JpegImagePlugin.py b/PIL/JpegImagePlugin.py index ffc14d2b6..550276c02 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 @@ -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 = TiffImagePlugin.ImageFileDirectory_v2(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 @@ -429,24 +420,21 @@ def _getexif(self): except (KeyError, TypeError): pass else: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(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 # 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: - info = TiffImagePlugin.ImageFileDirectory(head) + info = TiffImagePlugin.ImageFileDirectory_v2(head) info.load(file) - exif[0x8825] = gps = {} - for key, value in info.items(): - gps[key] = _fixup(value) + exif[0x8825] = dict(info) return exif @@ -464,23 +452,22 @@ 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 = TiffImagePlugin.ImageFileDirectory_v2(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] 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/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 = { diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index b0e7c9639..17426d4e2 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_V2, TYPES, TagInfo __version__ = "1.3.5" DEBUG = False # Needs to be merged with the new logging approach. @@ -60,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) @@ -67,25 +71,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,75 +134,78 @@ 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"), - - (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"), - + (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"), + (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"] @@ -223,409 +215,525 @@ 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 = {} - Exposes a dictionary interface of the tags in the directory - ImageFileDirectory[key] = value - value = ImageFileDirectory[key] +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. - Also contains a dictionary of tag types as read from the tiff - image file, 'ImageFileDirectory.tagtype' + Exposes a dictionary interface of the tags in the directory:: + 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. - Data Structures: - 'public' - * self.tagtype = {} Key: numerical tiff tag number - Value: integer corresponding to the data type from - `TiffTags.TYPES` + 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. - '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 + Data Structures: + * self.tagtype = {} + + * Key: numerical tiff tag number + * Value: integer corresponding to the data type from `~PIL.TiffTags.TYPES` - 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. + .. versionadded:: 3.0.0 + """ + """ + Documentation: + + 'internal' data structures: + * self._tags_v2 = {} Key: numerical tiff tag number + Value: decoded data, as tuple for multiple values + * self._tagdata = {} Key: numerical tiff tag number + Value: undecoded byte string from file + * self._tags_v1 = {} Key: numerical tiff tag number + Value: decoded data in the v1 format + + Tags will be found in the private attributes self._tagdata, and in + self._tags_v2 once decoded. + + 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): + """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. + + :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets + endianness. + :param prefix: Override the endianness of the file. """ - - def __init__(self, prefix=II): - """ - :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 + 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 self._prefix == II: + self._endian = "<" else: 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): + raise Exception("Not allowing setting of legacy api") 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_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 - 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()) + """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. """ - 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 dict((TAGS_V2.get(code, TagInfo()).name, value) + for code, value in self.items()) def __len__(self): - return len(self.tagdata) + len(self.tags) + return len(set(self._tagdata) | set(self._tags_v2)) 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 + if tag not in self._tags_v2: # unpack on the fly + data = self._tagdata[tag] + typ = self.tagtype[tag] + size, handler = self._load_dispatch[typ] + self[tag] = handler(self, data, self.legacy_api) # check type + val = self._tags_v2[tag] + if self.legacy_api and not isinstance(val, (tuple, bytes)): + val = val, + return val 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): - # 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 + self._setitem(tag, value, self.legacy_api) + + def _setitem(self, tag, value, legacy_api): + basetypes = (Number, bytes, str) + if bytes is str: + basetypes += unicode, + + info = TAGS_V2.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",'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 legacy_api and self.tagtype[tag] in [5, 10]: + values = values, + dest[tag], = values + else: + dest[tag] = values def __delitem__(self, tag): - self.tags.pop(tag, self.tagdata.pop(tag, None)) + self._tags_v2.pop(tag, None) + self._tags_v1.pop(tag, None) + self._tagdata.pop(tag, None) def __iter__(self): - return itertools.chain(self.tags.__iter__(), self.tagdata.__iter__()) + return iter(set(self._tagdata) | set(self._tags_v2)) - 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): + 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, 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)) + + list(map(_register_basic, + [(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, legacy_api=True): + 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. + def write_byte(self, data): return data - load_dispatch[1] = (1, load_byte) - def load_string(self, data): - if data[-1:] == b'\0': + @_register_loader(2, 1) + def load_string(self, data, legacy_api=True): + 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) + @_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" - 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_loader(5, 8) + 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) + for num, denom in zip(vals[::2], vals[1::2])) - 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) + @_register_writer(5) + def write_rational(self, *values): + return b"".join(self._pack("2L", *_limit_rational(frac, 2 ** 31)) + for frac in values) - def load_float(self, data): - a = array.array("f", data) - if self.prefix != native_prefix: - a.byteswap() - return tuple(a) - load_dispatch[11] = (4, load_float) - - def load_double(self, data): - a = array.array("d", data) - if self.prefix != native_prefix: - a.byteswap() - return tuple(a) - load_dispatch[12] = (8, load_double) - - def load_undefined(self, data): - # Untyped data + @_register_loader(7, 1) + def load_undefined(self, data, legacy_api=True): 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, 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) + 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 _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): - # load tag dictionary self.reset() - self.offset = fp.tell() + 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) - - 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=' ') - - try: - dispatch = 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 + tagname = TAGS_V2.get(tag, TagInfo()).name + typname = TYPES.get(typ, "unknown") + print("tag: %s (%d) - type: %s (%d)" % + (tagname, tag, typname, typ), end=" ") - size, handler = dispatch - - size = size * i32(ifd, 4) - - # Get and expand tag value - if size > 4: - here = fp.tell() - if DEBUG: - print("Tag Location: %s" % here) - fp.seek(i32(ifd, 8)) - if DEBUG: - print("Data Location: %s" % fp.tell()) - data = ImageFile._safe_read(fp, size) - fp.seek(here) - else: - data = ifd[8:8+size] - - 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 tag in (COLORMAP, IPTC_NAA_CHUNK, PHOTOSHOP_CHUNK, - ICCPROFILE, XMP): - 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] - 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))) + 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 - - self.next = i32(ifd) - - # save primitives - + def save(self, fp): - o16 = self.o16 - o32 = self.o32 + 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)) - 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_v2))) + entries = [] + offset = fp.tell() + len(self._tags_v2) * 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_v2.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_V2.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_v2._load_dispatch = _load_dispatch +ImageFileDirectory_v2._write_dispatch = _write_dispatch +for idx, name in TYPES.items(): + name = name.replace(" ", "_") + 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): + """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 + #insert deprecation warning here. + + tags = property(lambda self: self._tags_v1) + tagdata = property(lambda self: self._tagdata) + + @classmethod + def from_v2(cls, original): + """ Returns an + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + instance with the same data as is contained in the original + :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + instance. + + :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` + + """ + + 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): + """ 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: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2` + + """ + + 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, data, legacy), 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 ## # Image plugin for TIFF files. @@ -641,14 +749,14 @@ 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_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.i32(ifh, 4) + self.__first = self.__next = self.tag_v2.next self.__frame = -1 self.__fp = self.fp self._frame_pos = [] @@ -714,11 +822,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() @@ -737,19 +847,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 - self.tile_prefix = self.tag[JPEGTABLES] + # FIXME This will fail with more than one value + self.tile_prefix, = self.tag_v2[JPEGTABLES] elif compression == "packbits": args = rawmode elif compression == "tiff_lzw": args = rawmode - if 317 in self.tag: + if PREDICTOR in self.tag_v2: # Section 14: Differencing Predictor - self.decoderconfig = (self.tag[PREDICTOR][0],) + 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 @@ -772,7 +883,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: @@ -825,20 +936,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") - 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_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 = getscalar(PHOTOMETRIC_INTERPRETATION, 0) + photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0) - fillorder = getscalar(FILLORDER, 1) + fillorder = self.tag_v2.get(FILLORDER, 1) if DEBUG: print("*** Summary ***") @@ -848,20 +957,20 @@ class TiffImageFile(ImageFile.ImageFile): print("- fill_order:", fillorder) # size - xsize = getscalar(IMAGEWIDTH) - ysize = getscalar(IMAGELENGTH) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) self.size = xsize, ysize if DEBUG: print("- size:", self.size) - format = getscalar(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) @@ -878,8 +987,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_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.) @@ -888,7 +997,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_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 @@ -899,10 +1008,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 = getscalar(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", @@ -949,9 +1058,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) @@ -989,12 +1098,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 = getscalar(322) - h = getscalar(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 @@ -1018,7 +1127,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)) # # -------------------------------------------------------------------- @@ -1053,17 +1162,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: @@ -1071,31 +1169,26 @@ def _save(im, fp, filename): except KeyError: raise IOError("cannot write mode %s as TIFF" % im.mode) - ifd = ImageFileDirectory(prefix) + 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' # 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.o16(42) + ifd.o32(8)) - ifd[IMAGEWIDTH] = im.size[0] ifd[IMAGELENGTH] = im.size[1] # 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)) + if isinstance(info, ImageFileDirectory_v1): + info = info.to_v2() + for key in info: ifd[key] = info.get(key) try: ifd.tagtype[key] = info.tagtype[key] @@ -1104,44 +1197,42 @@ 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 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) + 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 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 +1260,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: @@ -1186,47 +1277,20 @@ 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 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..77a067981 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -17,291 +17,299 @@ # 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. +# +# id: (Name, Type, Length, enum_values) +# +TAGS_V2 = { -TAGS = { + 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}), - 254: "NewSubfileType", - 255: "SubfileType", - 256: "ImageWidth", - 257: "ImageLength", - 258: "BitsPerSample", + 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), - 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", + 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), - 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 + 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), - 263: "Thresholding", - 264: "CellWidth", - 265: "CellHeight", - 266: "FillOrder", - 269: "DocumentName", + 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), - 270: "ImageDescription", - 271: "Make", - 272: "Model", - 273: "StripOffsets", - 274: "Orientation", - 277: "SamplesPerPixel", - 278: "RowsPerStrip", - 279: "StripByteCounts", + 301: ("TransferFunction", 3, 0), + 305: ("Software", 2, 1), + 306: ("DateTime", 2, 1), - 280: "MinSampleValue", - 281: "MaxSampleValue", - 282: "XResolution", - 283: "YResolution", - 284: "PlanarConfiguration", - (284, 1): "Contigous", - (284, 2): "Separate", + 315: ("Artist", 2, 1), + 316: ("HostComputer", 2, 1), + 317: ("Predictor", 3, 1), + 318: ("WhitePoint", 5, 2), + 319: ("PrimaryChromaticies", 3, 6), - 285: "PageName", - 286: "XPosition", - 287: "YPosition", - 288: "FreeOffsets", - 289: "FreeByteCounts", + 320: ("ColorMap", 3, 0), + 321: ("HalftoneHints", 3, 2), + 322: ("TileWidth", 4, 1), + 323: ("TileLength", 4, 1), + 324: ("TileOffsets", 4, 0), + 325: ("TileByteCounts", 4, 0), - 290: "GrayResponseUnit", - 291: "GrayResponseCurve", - 292: "T4Options", - 293: "T6Options", - 296: "ResolutionUnit", - 297: "PageNumber", + 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), - 301: "TransferFunction", - 305: "Software", - 306: "DateTime", - - 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", + # FIXME add more tags here + 34665: ("ExifIFD", 3, 1), - # various extensions (should check specs for "official" names) - 33723: "IptcNaaInfo", - 34377: "PhotoshopInfo", + # 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), - # 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 + 50741: ("MakerNoteSafety", 3, 1, {"Unsafe": 0, "Safe": 1}), + 50780: ("BestQualityScale", 5, 1), + 50838: ("ImageJMetaDataByteCounts", 4, 1), + 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. + TAGS[k] = v[0] + if len(v) == 4: + for sk, sv in v[3].items(): + TAGS[(k, sv)] = sk + + TAGS_V2[k] = TagInfo(k, *v) + +_populate() ## -# Map type numbers to type names. +# Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = { +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", +# 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/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_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_libtiff.py b/Tests/test_file_libtiff.py index 8d5b383a9..f900b97cf 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1,8 +1,10 @@ from __future__ import print_function from helper import unittest, PillowTestCase, hopper, py3 +from ctypes import c_float import io import logging +import itertools import os from PIL import Image, TiffImagePlugin @@ -123,43 +125,45 @@ 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') + f = self.tempfile('temp.tiff') - img.save(f, tiffinfo=img.tag) + img.save(f, tiffinfo=img.tag) - loaded = Image.open(f) + if legacy_api: + original = img.tag.named() + else: + original = img.tag_v2.named() - original = img.tag.named() - reloaded = loaded.tag.named() + # PhotometricInterpretation is set from SAVE_INFO, + # not the original image. + ignored = ['StripByteCounts', 'RowsPerStrip', 'PageNumber', + 'PhotometricInterpretation'] - # PhotometricInterpretation is set from SAVE_INFO, - # not the original image. - ignored = [ - 'StripByteCounts', 'RowsPerStrip', - 'PageNumber', 'PhotometricInterpretation'] + loaded = Image.open(f) + if legacy_api: + reloaded = loaded.tag.named() + else: + reloaded = loaded.tag_v2.named() - for tag, value in reloaded.items(): - if tag not in ignored: - if tag.endswith('Resolution'): + for tag, value in itertools.chain(reloaded.items(), + original.items()): + if tag not in ignored: val = original[tag] - self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], - 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'): - val = reloaded[tag] - self.assert_almost_equal( - val[0][0]/val[0][1], value[0][0]/value[0][1], - msg="%s didn't roundtrip" % tag) - else: - self.assertEqual( - value, reloaded[tag], "%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') @@ -228,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 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 7d0871026..a221f15cc 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -74,14 +74,28 @@ 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._setup() + + #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 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_v2[X_RESOLUTION] = 71 + im.tag_v2[Y_RESOLUTION] = 71 + im._setup() + self.assertEqual(im.info['dpi'], (71., 71.)) + def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" @@ -89,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") @@ -98,7 +113,6 @@ class TestFileTiff(PillowTestCase): 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): @@ -206,7 +220,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) @@ -220,138 +233,82 @@ class TestFileTiff(PillowTestCase): # Arrange filename = "Tests/images/pil136.tiff" im = Image.open(filename) - - # Act - ret = im.ifd.as_dict() - - # Assert - self.assertIsInstance(ret, dict) - + # v2 interface 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,)}) + 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,)}) 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, b"abc") + for legacy_api in [False, True]: + ifd = TiffImagePlugin.ImageFileDirectory_v2() + data = b"abc" + ret = ifd.load_byte(data, legacy_api) + self.assertEqual(ret, b"abc" if legacy_api else (97, 98, 99)) def test_load_string(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abc\0" - - # Act - ret = ifd.load_string(data) - - # Assert + ret = ifd.load_string(data, False) self.assertEqual(ret, "abc") def test_load_float(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdabcd" - - # Act - ret = ifd.load_float(data) - - # Assert + ret = ifd.load_float(data, False) self.assertEqual(ret, (1.6777999408082104e+22, 1.6777999408082104e+22)) def test_load_double(self): - # Arrange - ifd = TiffImagePlugin.ImageFileDirectory() + ifd = TiffImagePlugin.ImageFileDirectory_v2() data = b"abcdefghabcdefgh" - - # Act - ret = ifd.load_double(data) - - # Assert + ret = ifd.load_double(data, False) 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__cvt_res_int(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res + def test__limit_rational_int(self): + from PIL.TiffImagePlugin import _limit_rational value = 34 - - # Act - ret = _cvt_res(value) - - # Assert + ret = _limit_rational(value, 65536) self.assertEqual(ret, (34, 1)) - def test__cvt_res_float(self): - # Arrange - from PIL.TiffImagePlugin import _cvt_res + def test__limit_rational_float(self): + from PIL.TiffImagePlugin import _limit_rational value = 22.3 - - # Act - ret = _cvt_res(value) - - # 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]) + ret = _limit_rational(value, 65536) + 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) @@ -361,52 +318,50 @@ 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.tags[X_RESOLUTION][0][0], 72) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 36) + + # 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): - # 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.tags[X_RESOLUTION][0][0], 36) - self.assertEqual(im.tag.tags[Y_RESOLUTION][0][0], 72) + + # 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__': diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index f71db0924..f2197ad04 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -1,8 +1,13 @@ +from __future__ import division + +import io +import struct + from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags -tag_ids = dict(zip(TiffTags.TAGS.values(), TiffTags.TAGS.keys())) +tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values()) class TestFileTiffMetadata(PillowTestCase): @@ -15,62 +20,95 @@ class TestFileTiffMetadata(PillowTestCase): img = hopper() + # 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 + # 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" + 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(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 + info[ImageDescription] = textdata + 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[50839], basetextdata + " ?") - self.assertAlmostEqual(loaded.tag[tag_ids['RollAngle']][0], floatdata, - places=5) - self.assertAlmostEqual(loaded.tag[tag_ids['YawAngle']][0], doubledata) + self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) + self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata)) + + 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) + loaded_double = loaded.tag[tag_ids['YawAngle']][0] + self.assertAlmostEqual(loaded_double, doubledata) + def test_read_metadata(self): 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,), - } + 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 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({'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 """ @@ -81,8 +119,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'] @@ -101,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() 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 ec3c49ecc..5d2cab518 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -461,17 +461,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 ~~~~~~~~~~~~~~~~~~ @@ -479,17 +499,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 diff --git a/encode.c b/encode.c index 6bdb8c71a..c46d78426 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)) { - 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)) { - int len,i; - float *floatav; - int *intav; - TRACE(("Setting from List: %d \n", (int)PyInt_AsLong(key))); - len = (int)PyList_Size(value); - if (len) { - if (PyInt_Check(PyList_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), - intav); - free(intav); - } - } else { - TRACE((" %d elements, setting as floats \n", len)); - floatav = malloc(sizeof(float)*len); - if (floatav) { - for (i=0;istate, - (ttag_t) PyInt_AsLong(key), - floatav); - free(floatav); - } - } - } } 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 (PyTuple_Check(value)) { + int len,i; + float *floatav; + int *intav; + TRACE(("Setting from Tuple: %d \n", (int)PyInt_AsLong(key))); + len = (int)PyTuple_Size(value); + if (len) { + 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), + len, intav); + free(intav); + } + } else if (PyFloat_Check(PyTuple_GetItem(value,0))) { + TRACE((" %d elements, setting as floats \n", len)); + floatav = malloc(sizeof(float)*len); + if (floatav) { + for (i=0;istate, + (ttag_t) PyInt_AsLong(key), + 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 { TRACE(("Unhandled type for key %d : %s \n", (int)PyInt_AsLong(key),