Added a rational class for TiffIFD that allows for 0/0

This commit is contained in:
wiredfool 2015-10-25 12:49:45 +00:00
parent c33fc39e76
commit deecbcd3a3
3 changed files with 72 additions and 10 deletions

View File

@ -47,9 +47,10 @@ from PIL import _binary
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
@ -215,8 +216,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 +225,64 @@ def _limit_rational(val, max_val):
_load_dispatch = {} _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
class IFDRational(Fraction):
""" 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
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
try:
if denominator == 1:
self._val = Fraction(value)
else:
self._val = Fraction(value, denominator)
except:
print(type(value), type(denominator))
raise
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))
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
@ -477,7 +535,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 +555,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]))
@ -1296,6 +1354,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

View File

@ -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.))

View File

@ -126,8 +126,10 @@ class TestFileTiffMetadata(PillowTestCase):
for tag, value in reloaded.items(): for tag, value in reloaded.items():
if tag not in ignored: if tag not in ignored:
self.assertEqual( self.assertEqual(original[tag],
original[tag], value, "%s didn't roundtrip" % 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: