Merge pull request #1531 from wiredfool/exif_div_zero

Divide by zero in Exif
This commit is contained in:
Hugo 2015-12-31 15:15:20 +02:00
commit 55a037c50b
6 changed files with 288 additions and 48 deletions

View File

@ -44,18 +44,21 @@ from __future__ import division, print_function
from PIL import Image, ImageFile
from PIL import ImagePalette
from PIL import _binary
from PIL import TiffTags
import collections
from fractions import Fraction
from numbers import Number, Rational
import io
import itertools
from numbers import Number
import os
import struct
import sys
import warnings
from .TiffTags import TAGS_V2, TYPES, TagInfo
from .TiffTags import TYPES, TagInfo
__version__ = "1.3.5"
DEBUG = False # Needs to be merged with the new logging approach.
@ -215,8 +218,7 @@ def _accept(prefix):
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)
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d
##
@ -225,6 +227,129 @@ def _limit_rational(val, max_val):
_load_dispatch = {}
_write_dispatch = {}
class IFDRational(Rational):
""" Implements a rational class where 0/0 is a legal value to match
the in the wild use of exif rationals.
e.g., DigitalZoomRatio - 0.00/0.00 indicates that no digital zoom was used
"""
""" If the denominator is 0, store this as a float('nan'), otherwise store
as a fractions.Fraction(). Delegate as appropriate
"""
__slots__ = ('_numerator', '_denominator', '_val')
def __init__(self, value, denominator=1):
"""
:param value: either an integer numerator, a
float/rational/other number, or an IFDRational
:param denominator: Optional integer denominator
"""
self._denominator = denominator
self._numerator = value
self._val = float(1)
if type(value) == Fraction:
self._numerator = value.numerator
self._denominator = value.denominator
self._val = value
if type(value) == IFDRational:
self._denominator = value.denominator
self._numerator = value.numerator
self._val = value._val
return
if denominator == 0:
self._val = float('nan')
return
elif denominator == 1:
if sys.hexversion < 0x2070000 and type(value) == float:
# python 2.6 is different.
self._val = Fraction.from_float(value)
else:
self._val = Fraction(value)
else:
self._val = Fraction(value, denominator)
@property
def numerator(a):
return a._numerator
@property
def denominator(a):
return a._denominator
def limit_rational(self, max_denominator):
"""
:param max_denominator: Integer, the maximum denominator value
:returns: Tuple of (numerator, denominator)
"""
if self.denominator == 0:
return (self.numerator, self.denominator)
f = self._val.limit_denominator(max_denominator)
return (f.numerator, f.denominator)
def __repr__(self):
return str(float(self._val))
def __hash__(self):
return self._val.__hash__()
def __eq__(self,other):
return self._val == other
def _delegate(op):
def delegate(self, *args):
return getattr(self._val,op)(*args)
return delegate
""" a = ['add','radd', 'sub', 'rsub','div', 'rdiv', 'mul', 'rmul',
'truediv', 'rtruediv', 'floordiv',
'rfloordiv','mod','rmod', 'pow','rpow', 'pos', 'neg',
'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero',
'ceil', 'floor', 'round']
print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a)
"""
__add__ = _delegate('__add__')
__radd__ = _delegate('__radd__')
__sub__ = _delegate('__sub__')
__rsub__ = _delegate('__rsub__')
__div__ = _delegate('__div__')
__rdiv__ = _delegate('__rdiv__')
__mul__ = _delegate('__mul__')
__rmul__ = _delegate('__rmul__')
__truediv__ = _delegate('__truediv__')
__rtruediv__ = _delegate('__rtruediv__')
__floordiv__ = _delegate('__floordiv__')
__rfloordiv__ = _delegate('__rfloordiv__')
__mod__ = _delegate('__mod__')
__rmod__ = _delegate('__rmod__')
__pow__ = _delegate('__pow__')
__rpow__ = _delegate('__rpow__')
__pos__ = _delegate('__pos__')
__neg__ = _delegate('__neg__')
__abs__ = _delegate('__abs__')
__trunc__ = _delegate('__trunc__')
__lt__ = _delegate('__lt__')
__gt__ = _delegate('__gt__')
__le__ = _delegate('__le__')
__ge__ = _delegate('__ge__')
__nonzero__ = _delegate('__nonzero__')
__ceil__ = _delegate('__ceil__')
__floor__ = _delegate('__floor__')
__round__ = _delegate('__round__')
class ImageFileDirectory_v2(collections.MutableMapping):
"""This class represents a TIFF tag directory. To speed things up, we
@ -338,7 +463,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
Returns the complete tag dictionary, with named tags where possible.
"""
return dict((TAGS_V2.get(code, TagInfo()).name, value)
return dict((TiffTags.lookup(code).name, value)
for code, value in self.items())
def __len__(self):
@ -370,15 +495,17 @@ class ImageFileDirectory_v2(collections.MutableMapping):
if bytes is str:
basetypes += unicode,
info = TAGS_V2.get(tag, TagInfo())
info = TiffTags.lookup(tag)
values = [value] if isinstance(value, basetypes) else value
if tag not in self.tagtype:
try:
if info.type:
self.tagtype[tag] = info.type
except KeyError:
else:
self.tagtype[tag] = 7
if all(isinstance(v, int) for v in values):
if all(isinstance(v, IFDRational) for v in values):
self.tagtype[tag] = 5
elif all(isinstance(v, int) for v in values):
if all(v < 2 ** 16 for v in values):
self.tagtype[tag] = 3
else:
@ -477,7 +604,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
@_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
combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom)
for num, denom in zip(vals[::2], vals[1::2]))
@ -497,7 +624,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
@_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
combine = lambda a, b: (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom)
for num, denom in zip(vals[::2], vals[1::2]))
@ -523,7 +650,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
for i in range(self._unpack("H", self._ensure_read(fp, 2))[0]):
tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12))
if DEBUG:
tagname = TAGS_V2.get(tag, TagInfo()).name
tagname = TiffTags.lookup(tag).name
typname = TYPES.get(typ, "unknown")
print("tag: %s (%d) - type: %s (%d)" %
(tagname, tag, typname, typ), end=" ")
@ -591,7 +718,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
values = value if isinstance(value, tuple) else (value,)
data = self._write_dispatch[typ](self, *values)
if DEBUG:
tagname = TAGS_V2.get(tag, TagInfo()).name
tagname = TiffTags.lookup(tag).name
typname = TYPES.get(typ, "unknown")
print("save: %s (%d) - type: %s (%d)" %
(tagname, tag, typname, typ), end=" ")
@ -994,16 +1121,10 @@ class TiffImageFile(ImageFile.ImageFile):
self.info["compression"] = self._compression
xres = self.tag_v2.get(X_RESOLUTION, (1, 1))
yres = self.tag_v2.get(Y_RESOLUTION, (1, 1))
xres = self.tag_v2.get(X_RESOLUTION,1)
yres = self.tag_v2.get(Y_RESOLUTION,1)
if xres and not isinstance(xres, tuple):
xres = (xres, 1.)
if yres and not isinstance(yres, tuple):
yres = (yres, 1.)
if xres and yres:
xres = xres[0] / (xres[1] or 1)
yres = yres[0] / (yres[1] or 1)
resunit = self.tag_v2.get(RESOLUTION_UNIT, 1)
if resunit == 2: # dots per inch
self.info["dpi"] = xres, yres
@ -1296,6 +1417,8 @@ def _save(im, fp, filename):
if k not in atts and k not in blocklist:
if isinstance(v, unicode if bytes is str else str):
atts[k] = v.encode('ascii', 'replace') + b"\0"
elif isinstance(v, IFDRational):
atts[k] = float(v)
else:
atts[k] = v

View File

@ -23,13 +23,25 @@ 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):
def __new__(cls, value=None, name="unknown", type=None, 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)
def lookup(tag):
"""
:param tag: Integer tag number
:returns: Taginfo namedtuple, From the TAGS_V2 info if possible,
otherwise just populating the value and name from TAGS.
If the tag is not recognized, "unknown" is returned for the name
"""
return TAGS_V2.get(tag, TagInfo(tag, TAGS.get(tag, 'unknown')))
##
# Map tag numbers to tag info.
#

View File

@ -84,9 +84,9 @@ class TestFileTiff(PillowTestCase):
self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple)
self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple)
# v2 api
self.assertIsInstance(im.tag_v2[X_RESOLUTION], float)
self.assertIsInstance(im.tag_v2[Y_RESOLUTION], float)
#v2 api
self.assert_(isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational))
self.assert_(isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational))
self.assertEqual(im.info['dpi'], (72., 72.))

View File

@ -6,6 +6,7 @@ import struct
from helper import unittest, PillowTestCase, hopper
from PIL import Image, TiffImagePlugin, TiffTags
from PIL.TiffImagePlugin import _limit_rational, IFDRational
tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values())
@ -73,7 +74,7 @@ class TestFileTiffMetadata(PillowTestCase):
def test_read_metadata(self):
img = Image.open('Tests/images/hopper_g4.tif')
self.assertEqual({'YResolution': 4294967295 / 113653537,
self.assertEqual({'YResolution': IFDRational(4294967295, 113653537),
'PlanarConfiguration': 1,
'BitsPerSample': (1,),
'ImageLength': 128,
@ -83,7 +84,7 @@ class TestFileTiffMetadata(PillowTestCase):
'ResolutionUnit': 3,
'PhotometricInterpretation': 0,
'PageNumber': (0, 1),
'XResolution': 4294967295 / 113653537,
'XResolution': IFDRational(4294967295, 113653537),
'ImageWidth': 128,
'Orientation': 1,
'StripByteCounts': (1968,),
@ -121,13 +122,32 @@ class TestFileTiffMetadata(PillowTestCase):
original = img.tag_v2.named()
reloaded = loaded.tag_v2.named()
ignored = [
'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets']
for k,v in original.items():
if type(v) == IFDRational:
original[k] = IFDRational(*_limit_rational(v,2**31))
if type(v) == tuple and \
type(v[0]) == IFDRational:
original[k] = tuple([IFDRational(
*_limit_rational(elt, 2**31)) for elt in v])
ignored = ['StripByteCounts', 'RowsPerStrip',
'PageNumber', 'StripOffsets']
for tag, value in reloaded.items():
if tag not in ignored:
self.assertEqual(
original[tag], value, "%s didn't roundtrip" % tag)
if tag in ignored: continue
if (type(original[tag]) == tuple
and type(original[tag][0]) == IFDRational):
# Need to compare element by element in the tuple,
# not comparing tuples of object references
self.assert_deep_equal(original[tag],
value,
"%s didn't roundtrip, %s, %s" %
(tag, original[tag], value))
else:
self.assertEqual(original[tag],
value,
"%s didn't roundtrip, %s, %s" %
(tag, original[tag], value))
for tag, value in original.items():
if tag not in ignored:
@ -165,6 +185,20 @@ class TestFileTiffMetadata(PillowTestCase):
self.assertEqual(im.tag_v2.tagtype[34675], 1)
self.assertTrue(im.info['icc_profile'])
def test_exif_div_zero(self):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()
info[41988] = TiffImagePlugin.IFDRational(0,0)
out = self.tempfile('temp.tiff')
im.save(out, tiffinfo=info, compression='raw')
reloaded = Image.open(out)
self.assertEqual(0, reloaded.tag_v2[41988][0].numerator)
self.assertEqual(0, reloaded.tag_v2[41988][0].denominator)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,61 @@
from __future__ import print_function
from helper import PillowTestCase, hopper
from PIL import TiffImagePlugin, Image
from PIL.TiffImagePlugin import IFDRational
from fractions import Fraction
class Test_IFDRational(PillowTestCase):
def _test_equal(self, num, denom, target):
t = IFDRational(num, denom)
self.assertEqual(target, t)
self.assertEqual(t, target)
def test_sanity(self):
self._test_equal(1, 1, 1)
self._test_equal(1, 1, Fraction(1,1))
self._test_equal(2, 2, 1)
self._test_equal(1.0, 1, Fraction(1,1))
self._test_equal(Fraction(1,1), 1, Fraction(1,1))
self._test_equal(IFDRational(1,1), 1, 1)
self._test_equal(1, 2, Fraction(1,2))
self._test_equal(1, 2, IFDRational(1,2))
def test_nonetype(self):
" Fails if the _delegate function doesn't return a valid function"
xres = IFDRational(72)
yres = IFDRational(72)
self.assert_(xres._val is not None)
self.assert_(xres.numerator is not None)
self.assert_(xres.denominator is not None)
self.assert_(yres._val is not None)
self.assert_(xres and 1)
self.assert_(xres and yres)
def test_ifd_rational_save(self):
for libtiff in (True, False):
TiffImagePlugin.WRITE_LIBTIFF = libtiff
im = hopper()
out = self.tempfile('temp.tiff')
res = IFDRational(301,1)
im.save(out, dpi=(res,res), compression='raw')
reloaded = Image.open(out)
self.assertEqual(float(IFDRational(301,1)),
float(reloaded.tag_v2[282]))

View File

@ -496,10 +496,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
.. 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.
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 :py:class:`~PIL.TiffImagePlugin.IFDRational`
object.
.. versionadded:: 3.0.0
@ -529,20 +531,27 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum
.. versionadded:: 2.3.0
For compatibility with legacy code, a
`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may be passed
in this field. However, this is deprecated.
Metadata values that are of the rational type should be passed in
using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object.
..versionadded:: 3.0.0
.. versionadded:: 3.1.0
For compatibility with legacy code, a
:py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may
be passed in this field. However, this is deprecated.
.. versionadded:: 3.0.0
**compression**
A string containing the desired compression method for the
file. (valid only with libtiff installed) Valid compression
methods are: ``[None, "tiff_ccitt", "group3", "group4",
"tiff_jpeg", "tiff_adobe_deflate", "tiff_thunderscan",
"tiff_deflate", "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]``
methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``,
``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``,
``"tiff_thunderscan"``, ``"tiff_deflate"``, ``"tiff_sgilog"``,
``"tiff_sgilog24"``, ``"tiff_raw_16"``
These arguments to set the tiff header fields are an alternative to using the general tags available through tiffinfo.
These arguments to set the tiff header fields are an alternative to
using the general tags available through tiffinfo.
**description**
@ -564,10 +573,11 @@ These arguments to set the tiff header fields are an alternative to using the ge
**y_resolution**
**dpi**
Either a Float, Integer, or 2 tuple of (numerator,
denominator). Resolution implies an equal x and y resolution, dpi
also implies a unit of inches.
**dpi**
Either a Float, 2 tuple of (numerator, denominator) or a
:py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies
an equal x and y resolution, dpi also implies a unit of inches.
WebP
^^^^