From deecbcd3a3c57302d3a17757297e4336b4c263cf Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 25 Oct 2015 12:49:45 +0000 Subject: [PATCH] Added a rational class for TiffIFD that allows for 0/0 --- PIL/TiffImagePlugin.py | 70 +++++++++++++++++++++++++++++--- Tests/test_file_tiff.py | 6 +-- Tests/test_file_tiff_metadata.py | 6 ++- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 298c48759..0e6efc416 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -47,9 +47,10 @@ from PIL import _binary 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 @@ -215,8 +216,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 +225,64 @@ def _limit_rational(val, max_val): _load_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): """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) 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 +555,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])) @@ -1296,6 +1354,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 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 312daea56..5c5958184 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -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.)) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index b0f4e402a..9c976e733 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -126,8 +126,10 @@ class TestFileTiffMetadata(PillowTestCase): for tag, value in reloaded.items(): if tag not in ignored: - self.assertEqual( - original[tag], value, "%s didn't roundtrip" % tag) + 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: