mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-28 02:04:36 +03:00
Merge pull request #1531 from wiredfool/exif_div_zero
Divide by zero in Exif
This commit is contained in:
commit
55a037c50b
|
@ -44,18 +44,21 @@ from __future__ import division, print_function
|
||||||
from PIL import Image, ImageFile
|
from PIL import Image, ImageFile
|
||||||
from PIL import ImagePalette
|
from PIL import ImagePalette
|
||||||
from PIL import _binary
|
from PIL import _binary
|
||||||
|
from PIL import TiffTags
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
|
from numbers import Number, Rational
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
from numbers import Number
|
|
||||||
import os
|
import os
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from .TiffTags import TAGS_V2, TYPES, TagInfo
|
from .TiffTags import TYPES, TagInfo
|
||||||
|
|
||||||
|
|
||||||
__version__ = "1.3.5"
|
__version__ = "1.3.5"
|
||||||
DEBUG = False # Needs to be merged with the new logging approach.
|
DEBUG = False # Needs to be merged with the new logging approach.
|
||||||
|
@ -215,8 +218,7 @@ def _accept(prefix):
|
||||||
|
|
||||||
def _limit_rational(val, max_val):
|
def _limit_rational(val, max_val):
|
||||||
inv = abs(val) > 1
|
inv = abs(val) > 1
|
||||||
f = Fraction.from_float(1 / val if inv else val).limit_denominator(max_val)
|
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
|
||||||
n_d = (f.numerator, f.denominator)
|
|
||||||
return n_d[::-1] if inv else n_d
|
return n_d[::-1] if inv else n_d
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -225,6 +227,129 @@ def _limit_rational(val, max_val):
|
||||||
_load_dispatch = {}
|
_load_dispatch = {}
|
||||||
_write_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):
|
class ImageFileDirectory_v2(collections.MutableMapping):
|
||||||
"""This class represents a TIFF tag directory. To speed things up, we
|
"""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.
|
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())
|
for code, value in self.items())
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
|
@ -370,15 +495,17 @@ class ImageFileDirectory_v2(collections.MutableMapping):
|
||||||
if bytes is str:
|
if bytes is str:
|
||||||
basetypes += unicode,
|
basetypes += unicode,
|
||||||
|
|
||||||
info = TAGS_V2.get(tag, TagInfo())
|
info = TiffTags.lookup(tag)
|
||||||
values = [value] if isinstance(value, basetypes) else value
|
values = [value] if isinstance(value, basetypes) else value
|
||||||
|
|
||||||
if tag not in self.tagtype:
|
if tag not in self.tagtype:
|
||||||
try:
|
if info.type:
|
||||||
self.tagtype[tag] = info.type
|
self.tagtype[tag] = info.type
|
||||||
except KeyError:
|
else:
|
||||||
self.tagtype[tag] = 7
|
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):
|
if all(v < 2 ** 16 for v in values):
|
||||||
self.tagtype[tag] = 3
|
self.tagtype[tag] = 3
|
||||||
else:
|
else:
|
||||||
|
@ -477,7 +604,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
|
||||||
@_register_loader(5, 8)
|
@_register_loader(5, 8)
|
||||||
def load_rational(self, data, legacy_api=True):
|
def load_rational(self, data, legacy_api=True):
|
||||||
vals = self._unpack("{0}L".format(len(data) // 4), data)
|
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)
|
return tuple(combine(num, denom)
|
||||||
for num, denom in zip(vals[::2], vals[1::2]))
|
for num, denom in zip(vals[::2], vals[1::2]))
|
||||||
|
|
||||||
|
@ -497,7 +624,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
|
||||||
@_register_loader(10, 8)
|
@_register_loader(10, 8)
|
||||||
def load_signed_rational(self, data, legacy_api=True):
|
def load_signed_rational(self, data, legacy_api=True):
|
||||||
vals = self._unpack("{0}l".format(len(data) // 4), data)
|
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)
|
return tuple(combine(num, denom)
|
||||||
for num, denom in zip(vals[::2], vals[1::2]))
|
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]):
|
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))
|
tag, typ, count, data = self._unpack("HHL4s", self._ensure_read(fp, 12))
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
tagname = TAGS_V2.get(tag, TagInfo()).name
|
tagname = TiffTags.lookup(tag).name
|
||||||
typname = TYPES.get(typ, "unknown")
|
typname = TYPES.get(typ, "unknown")
|
||||||
print("tag: %s (%d) - type: %s (%d)" %
|
print("tag: %s (%d) - type: %s (%d)" %
|
||||||
(tagname, tag, typname, typ), end=" ")
|
(tagname, tag, typname, typ), end=" ")
|
||||||
|
@ -591,7 +718,7 @@ class ImageFileDirectory_v2(collections.MutableMapping):
|
||||||
values = value if isinstance(value, tuple) else (value,)
|
values = value if isinstance(value, tuple) else (value,)
|
||||||
data = self._write_dispatch[typ](self, *values)
|
data = self._write_dispatch[typ](self, *values)
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
tagname = TAGS_V2.get(tag, TagInfo()).name
|
tagname = TiffTags.lookup(tag).name
|
||||||
typname = TYPES.get(typ, "unknown")
|
typname = TYPES.get(typ, "unknown")
|
||||||
print("save: %s (%d) - type: %s (%d)" %
|
print("save: %s (%d) - type: %s (%d)" %
|
||||||
(tagname, tag, typname, typ), end=" ")
|
(tagname, tag, typname, typ), end=" ")
|
||||||
|
@ -994,16 +1121,10 @@ class TiffImageFile(ImageFile.ImageFile):
|
||||||
|
|
||||||
self.info["compression"] = self._compression
|
self.info["compression"] = self._compression
|
||||||
|
|
||||||
xres = self.tag_v2.get(X_RESOLUTION, (1, 1))
|
xres = self.tag_v2.get(X_RESOLUTION,1)
|
||||||
yres = self.tag_v2.get(Y_RESOLUTION, (1, 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:
|
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)
|
resunit = self.tag_v2.get(RESOLUTION_UNIT, 1)
|
||||||
if resunit == 2: # dots per inch
|
if resunit == 2: # dots per inch
|
||||||
self.info["dpi"] = xres, yres
|
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 k not in atts and k not in blocklist:
|
||||||
if isinstance(v, unicode if bytes is str else str):
|
if isinstance(v, unicode if bytes is str else str):
|
||||||
atts[k] = v.encode('ascii', 'replace') + b"\0"
|
atts[k] = v.encode('ascii', 'replace') + b"\0"
|
||||||
|
elif isinstance(v, IFDRational):
|
||||||
|
atts[k] = float(v)
|
||||||
else:
|
else:
|
||||||
atts[k] = v
|
atts[k] = v
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,25 @@ 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=4, length=0, enum=None):
|
def __new__(cls, value=None, name="unknown", type=None, length=0, 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 {})
|
||||||
|
|
||||||
def cvt_enum(self, value):
|
def cvt_enum(self, value):
|
||||||
return self.enum.get(value, 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.
|
# Map tag numbers to tag info.
|
||||||
#
|
#
|
||||||
|
|
|
@ -84,9 +84,9 @@ class TestFileTiff(PillowTestCase):
|
||||||
self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple)
|
self.assertIsInstance(im.tag[X_RESOLUTION][0], tuple)
|
||||||
self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple)
|
self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple)
|
||||||
|
|
||||||
# v2 api
|
#v2 api
|
||||||
self.assertIsInstance(im.tag_v2[X_RESOLUTION], float)
|
self.assert_(isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational))
|
||||||
self.assertIsInstance(im.tag_v2[Y_RESOLUTION], float)
|
self.assert_(isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational))
|
||||||
|
|
||||||
self.assertEqual(im.info['dpi'], (72., 72.))
|
self.assertEqual(im.info['dpi'], (72., 72.))
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import struct
|
||||||
from helper import unittest, PillowTestCase, hopper
|
from helper import unittest, PillowTestCase, hopper
|
||||||
|
|
||||||
from PIL import Image, TiffImagePlugin, TiffTags
|
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())
|
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):
|
def test_read_metadata(self):
|
||||||
img = Image.open('Tests/images/hopper_g4.tif')
|
img = Image.open('Tests/images/hopper_g4.tif')
|
||||||
|
|
||||||
self.assertEqual({'YResolution': 4294967295 / 113653537,
|
self.assertEqual({'YResolution': IFDRational(4294967295, 113653537),
|
||||||
'PlanarConfiguration': 1,
|
'PlanarConfiguration': 1,
|
||||||
'BitsPerSample': (1,),
|
'BitsPerSample': (1,),
|
||||||
'ImageLength': 128,
|
'ImageLength': 128,
|
||||||
|
@ -83,7 +84,7 @@ class TestFileTiffMetadata(PillowTestCase):
|
||||||
'ResolutionUnit': 3,
|
'ResolutionUnit': 3,
|
||||||
'PhotometricInterpretation': 0,
|
'PhotometricInterpretation': 0,
|
||||||
'PageNumber': (0, 1),
|
'PageNumber': (0, 1),
|
||||||
'XResolution': 4294967295 / 113653537,
|
'XResolution': IFDRational(4294967295, 113653537),
|
||||||
'ImageWidth': 128,
|
'ImageWidth': 128,
|
||||||
'Orientation': 1,
|
'Orientation': 1,
|
||||||
'StripByteCounts': (1968,),
|
'StripByteCounts': (1968,),
|
||||||
|
@ -121,13 +122,32 @@ class TestFileTiffMetadata(PillowTestCase):
|
||||||
original = img.tag_v2.named()
|
original = img.tag_v2.named()
|
||||||
reloaded = loaded.tag_v2.named()
|
reloaded = loaded.tag_v2.named()
|
||||||
|
|
||||||
ignored = [
|
for k,v in original.items():
|
||||||
'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets']
|
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():
|
for tag, value in reloaded.items():
|
||||||
if tag not in ignored:
|
if tag in ignored: continue
|
||||||
self.assertEqual(
|
if (type(original[tag]) == tuple
|
||||||
original[tag], value, "%s didn't roundtrip" % tag)
|
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():
|
for tag, value in original.items():
|
||||||
if tag not in ignored:
|
if tag not in ignored:
|
||||||
|
@ -165,6 +185,20 @@ class TestFileTiffMetadata(PillowTestCase):
|
||||||
self.assertEqual(im.tag_v2.tagtype[34675], 1)
|
self.assertEqual(im.tag_v2.tagtype[34675], 1)
|
||||||
self.assertTrue(im.info['icc_profile'])
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
61
Tests/test_tiff_ifdrational.py
Normal file
61
Tests/test_tiff_ifdrational.py
Normal 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]))
|
||||||
|
|
|
@ -496,10 +496,12 @@ The :py:meth:`~PIL.Image.Image.open` method sets the following
|
||||||
.. versionadded:: 1.1.5
|
.. versionadded:: 1.1.5
|
||||||
|
|
||||||
|
|
||||||
The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary of
|
The :py:attr:`~PIL.Image.Image.tag_v2` attribute contains a dictionary
|
||||||
TIFF metadata. The keys are numerical indexes from `~PIL.TiffTags.TAGS_V2`.
|
of TIFF metadata. The keys are numerical indexes from
|
||||||
Values are strings or numbers for single items, multiple values are returned
|
`~PIL.TiffTags.TAGS_V2`. Values are strings or numbers for single
|
||||||
in a tuple of values. Rational numbers are returned as a single value.
|
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
|
.. 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
|
.. versionadded:: 2.3.0
|
||||||
|
|
||||||
For compatibility with legacy code, a
|
Metadata values that are of the rational type should be passed in
|
||||||
`~PIL.TiffImagePlugin.ImageFileDirectory_v1` object may be passed
|
using a :py:class:`~PIL.TiffImagePlugin.IFDRational` object.
|
||||||
in this field. However, this is deprecated.
|
|
||||||
|
|
||||||
..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**
|
**compression**
|
||||||
A string containing the desired compression method for the
|
A string containing the desired compression method for the
|
||||||
file. (valid only with libtiff installed) Valid compression
|
file. (valid only with libtiff installed) Valid compression
|
||||||
methods are: ``[None, "tiff_ccitt", "group3", "group4",
|
methods are: ``None``, ``"tiff_ccitt"``, ``"group3"``,
|
||||||
"tiff_jpeg", "tiff_adobe_deflate", "tiff_thunderscan",
|
``"group4"``, ``"tiff_jpeg"``, ``"tiff_adobe_deflate"``,
|
||||||
"tiff_deflate", "tiff_sgilog", "tiff_sgilog24", "tiff_raw_16"]``
|
``"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**
|
**description**
|
||||||
|
|
||||||
|
@ -565,9 +574,10 @@ These arguments to set the tiff header fields are an alternative to using the ge
|
||||||
**y_resolution**
|
**y_resolution**
|
||||||
|
|
||||||
**dpi**
|
**dpi**
|
||||||
Either a Float, Integer, or 2 tuple of (numerator,
|
Either a Float, 2 tuple of (numerator, denominator) or a
|
||||||
denominator). Resolution implies an equal x and y resolution, dpi
|
:py:class:`~PIL.TiffImagePlugin.IFDRational`. Resolution implies
|
||||||
also implies a unit of inches.
|
an equal x and y resolution, dpi also implies a unit of inches.
|
||||||
|
|
||||||
|
|
||||||
WebP
|
WebP
|
||||||
^^^^
|
^^^^
|
||||||
|
|
Loading…
Reference in New Issue
Block a user