Merge pull request #2719 from wiredfool/issue_2278

Fixes for Issues #2278 and #2006, value error in exif/tiff ifd
This commit is contained in:
wiredfool 2017-09-19 14:26:29 +01:00 committed by GitHub
commit a0ce5740d5
5 changed files with 82 additions and 11 deletions

View File

@ -550,11 +550,28 @@ class ImageFileDirectory_v2(collections.MutableMapping):
dest = self._tags_v1 if legacy_api else self._tags_v2 dest = self._tags_v1 if legacy_api else self._tags_v2
if info.length == 1: # Three branches:
if legacy_api and self.tagtype[tag] in [5, 10]: # Spec'd length == 1, Actual length 1, store as element
# Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
# No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
# Don't mess with the legacy api, since it's frozen.
if ((info.length == 1) or
(info.length is None and len(values) == 1 and not legacy_api)):
# Don't mess with the legacy api, since it's frozen.
if legacy_api and self.tagtype[tag] in [5, 10]: # rationals
values = values, values = values,
dest[tag], = values try:
dest[tag], = values
except ValueError:
# We've got a builtin tag with 1 expected entry
warnings.warn(
"Metadata Warning, tag %s had too many entries: %s, expected 1" % (
tag, len(values)))
dest[tag] = values[0]
else: else:
# Spec'd length > 1 or undefined
# Unspec'd, and length > 1
dest[tag] = values dest[tag] = values
def __delitem__(self, tag): def __delitem__(self, tag):
@ -1011,8 +1028,10 @@ class TiffImageFile(ImageFile.ImageFile):
args = rawmode, "" args = rawmode, ""
if JPEGTABLES in self.tag_v2: if JPEGTABLES in self.tag_v2:
# Hack to handle abbreviated JPEG headers # Hack to handle abbreviated JPEG headers
# FIXME This will fail with more than one value # Definition of JPEGTABLES is that the count
self.tile_prefix, = self.tag_v2[JPEGTABLES] # is the number of bytes in the tables datastream
# so, it should always be 1 in our tag info
self.tile_prefix = self.tag_v2[JPEGTABLES]
elif compression == "packbits": elif compression == "packbits":
args = rawmode args = rawmode
elif compression == "tiff_lzw": elif compression == "tiff_lzw":

View File

@ -23,7 +23,7 @@ from collections import namedtuple
class TagInfo(namedtuple("_TagInfo", "value name type length enum")): class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
__slots__ = [] __slots__ = []
def __new__(cls, value=None, name="unknown", type=None, length=0, enum=None): def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None):
return super(TagInfo, cls).__new__( return super(TagInfo, cls).__new__(
cls, value, name, type, length, enum or {}) cls, value, name, type, length, enum or {})
@ -142,6 +142,8 @@ TAGS_V2 = {
341: ("SMaxSampleValue", DOUBLE, 0), 341: ("SMaxSampleValue", DOUBLE, 0),
342: ("TransferRange", SHORT, 6), 342: ("TransferRange", SHORT, 6),
347: ("JPEGTables", UNDEFINED, 1),
# obsolete JPEG tags # obsolete JPEG tags
512: ("JPEGProc", SHORT, 1), 512: ("JPEGProc", SHORT, 1),
513: ("JPEGInterchangeFormat", LONG, 1), 513: ("JPEGInterchangeFormat", LONG, 1),
@ -158,7 +160,10 @@ TAGS_V2 = {
531: ("YCbCrPositioning", SHORT, 1), 531: ("YCbCrPositioning", SHORT, 1),
532: ("ReferenceBlackWhite", LONG, 0), 532: ("ReferenceBlackWhite", LONG, 0),
700: ('XMP', BYTE, 1),
33432: ("Copyright", ASCII, 1), 33432: ("Copyright", ASCII, 1),
34377: ('PhotoshopInfo', BYTE, 1),
# FIXME add more tags here # FIXME add more tags here
34665: ("ExifIFD", SHORT, 1), 34665: ("ExifIFD", SHORT, 1),
@ -188,8 +193,8 @@ TAGS_V2 = {
50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}), 50741: ("MakerNoteSafety", SHORT, 1, {"Unsafe": 0, "Safe": 1}),
50780: ("BestQualityScale", RATIONAL, 1), 50780: ("BestQualityScale", RATIONAL, 1),
50838: ("ImageJMetaDataByteCounts", LONG, 1), 50838: ("ImageJMetaDataByteCounts", LONG, 0), # Can be more than one
50839: ("ImageJMetaData", UNDEFINED, 1) 50839: ("ImageJMetaData", UNDEFINED, 1) # see Issue #2006
} }
# Legacy Tags structure # Legacy Tags structure

BIN
Tests/images/issue_2278.tif Normal file

Binary file not shown.

View File

@ -56,7 +56,7 @@ class TestFileTiffMetadata(PillowTestCase):
loaded = Image.open(f) loaded = Image.open(f)
self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),)) self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (len(bindata),))
self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], len(bindata)) self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (len(bindata),))
self.assertEqual(loaded.tag[ImageJMetaData], bindata) self.assertEqual(loaded.tag[ImageJMetaData], bindata)
self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata) self.assertEqual(loaded.tag_v2[ImageJMetaData], bindata)
@ -69,6 +69,16 @@ class TestFileTiffMetadata(PillowTestCase):
loaded_double = loaded.tag[tag_ids['YawAngle']][0] loaded_double = loaded.tag[tag_ids['YawAngle']][0]
self.assertAlmostEqual(loaded_double, doubledata) self.assertAlmostEqual(loaded_double, doubledata)
# check with 2 element ImageJMetaDataByteCounts, issue #2006
info[ImageJMetaDataByteCounts] = (8, len(bindata) - 8)
img.save(f, tiffinfo=info)
loaded = Image.open(f)
self.assertEqual(loaded.tag[ImageJMetaDataByteCounts], (8, len(bindata) - 8))
self.assertEqual(loaded.tag_v2[ImageJMetaDataByteCounts], (8, len(bindata) - 8))
def test_read_metadata(self): def test_read_metadata(self):
img = Image.open('Tests/images/hopper_g4.tif') img = Image.open('Tests/images/hopper_g4.tif')
@ -202,8 +212,8 @@ class TestFileTiffMetadata(PillowTestCase):
im.save(out, tiffinfo=info, compression='raw') im.save(out, tiffinfo=info, compression='raw')
reloaded = Image.open(out) reloaded = Image.open(out)
self.assertEqual(0, reloaded.tag_v2[41988][0].numerator) self.assertEqual(0, reloaded.tag_v2[41988].numerator)
self.assertEqual(0, reloaded.tag_v2[41988][0].denominator) self.assertEqual(0, reloaded.tag_v2[41988].denominator)
def test_expty_values(self): def test_expty_values(self):
data = io.BytesIO( data = io.BytesIO(
@ -220,6 +230,27 @@ class TestFileTiffMetadata(PillowTestCase):
self.fail("Should not be struct value error there.") self.fail("Should not be struct value error there.")
self.assertIn(33432, info) self.assertIn(33432, info)
def test_PhotoshopInfo(self):
im = Image.open('Tests/images/issue_2278.tif')
self.assertIsInstance(im.tag_v2[34377], bytes)
out = self.tempfile('temp.tiff')
im.save(out)
reloaded = Image.open(out)
self.assertIsInstance(reloaded.tag_v2[34377], bytes)
def test_too_many_entries(self):
ifd = TiffImagePlugin.ImageFileDirectory_v2()
# 277: ("SamplesPerPixel", SHORT, 1),
ifd._tagdata[277] = struct.pack('hh', 4,4)
ifd.tagtype[277] = TiffTags.SHORT
try:
self.assert_warning(UserWarning, lambda: ifd[277])
except ValueError:
self.fail("Invalid Metadata count should not cause a Value Error.")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -46,6 +46,22 @@ This release contains several performance improvements:
using a recent version of libjpeg-turbo. using a recent version of libjpeg-turbo.
TIFF Metadata Changes
=====================
* TIFF tags with unknown type/quantity now default to being bare
values if they are 1 element, where previously they would be a
single element tuple. This is only with the new api, not the legacy
api. This normalizes the handling of fields, so that the metadata
with inferred or image specified counts are handled the same as
metadata with count specified in the TIFF spec.
* The ``PhotoshopInfo``, ``XMP``, and ``JPEGTables`` tags now have a
defined type (bytes) and a count of 1.
* The ``ImageJMetaDataByteCounts`` tag now has an arbitrary number of
items, as there can be multiple items, one for UTF-8, and one for
UTF-16.
Core Image API Changes Core Image API Changes
====================== ======================