From deecbcd3a3c57302d3a17757297e4336b4c263cf Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 25 Oct 2015 12:49:45 +0000 Subject: [PATCH 01/12] 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: From 3bbb9e676ff09a7a62c45175cf86fba31e279f6f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 25 Oct 2015 14:17:50 +0000 Subject: [PATCH 02/12] value based equivalence --- PIL/TiffImagePlugin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 0e6efc416..18793982d 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -282,6 +282,13 @@ class IFDRational(Fraction): def __repr__(self): return str(float(self._val)) + + def __eq__(self,other): + if type(other) == float: + return float(self) == other + if type(other) == int: + return float(self) == float(int(self)) and int(self) == other + return float(self) == float(other) class ImageFileDirectory_v2(collections.MutableMapping): From f9fe4da8b252dab9f3756c4b93793c781f836377 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 25 Oct 2015 14:49:52 +0000 Subject: [PATCH 03/12] Make IFDRational hashable --- PIL/TiffImagePlugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 18793982d..ee5bd098a 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -283,12 +283,16 @@ class IFDRational(Fraction): def __repr__(self): return str(float(self._val)) + def __hash__(self): + return self._val.__hash__() + def __eq__(self,other): if type(other) == float: return float(self) == other if type(other) == int: return float(self) == float(int(self)) and int(self) == other return float(self) == float(other) + class ImageFileDirectory_v2(collections.MutableMapping): From 722ee8240bed099accfabef8fff5085978070c3d Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 18 Nov 2015 08:51:57 -0800 Subject: [PATCH 04/12] Inherit from Rational instead of Fraction, some basic tests. Fixes Py2.6 --- PIL/TiffImagePlugin.py | 78 +++++++++++++++++++++++++--------- Tests/test_tiff_ifdrational.py | 46 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 Tests/test_tiff_ifdrational.py diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index ee5bd098a..1cb4e2c9f 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -225,7 +225,7 @@ def _limit_rational(val, max_val): _load_dispatch = {} _write_dispatch = {} -class IFDRational(Fraction): +class IFDRational(Rational): """ Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -247,6 +247,12 @@ class IFDRational(Fraction): """ 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 @@ -258,14 +264,16 @@ class IFDRational(Fraction): self._val = float('nan') return - try: - if denominator == 1: - self._val = Fraction(value) + + 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, denominator) - except: - print(type(value), type(denominator)) - raise + self._val = Fraction(value) + else: + self._val = Fraction(value, denominator) + def limit_rational(self, max_denominator): """ @@ -287,11 +295,45 @@ class IFDRational(Fraction): return self._val.__hash__() def __eq__(self,other): - if type(other) == float: - return float(self) == other - if type(other) == int: - return float(self) == float(int(self)) and int(self) == other - return float(self) == float(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'] + 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__') @@ -1063,16 +1105,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 diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py new file mode 100644 index 000000000..05f66d5c4 --- /dev/null +++ b/Tests/test_tiff_ifdrational.py @@ -0,0 +1,46 @@ +from __future__ import print_function + +from helper import PillowTestCase + +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) + From 79608bd7624efe008f664948d6d958437f820bdd Mon Sep 17 00:00:00 2001 From: wiredfool Date: Wed, 18 Nov 2015 09:00:15 -0800 Subject: [PATCH 05/12] Make numerator/denominator read only --- PIL/TiffImagePlugin.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 1cb4e2c9f..3cf214edd 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -237,7 +237,7 @@ class IFDRational(Rational): """ - __slots__ = ('numerator', 'denominator', '_val') + __slots__ = ('_numerator', '_denominator', '_val') def __init__(self, value, denominator=1): """ @@ -245,18 +245,18 @@ class IFDRational(Rational): float/rational/other number, or an IFDRational :param denominator: Optional integer denominator """ - self.denominator = denominator - self.numerator = value + self._denominator = denominator + self._numerator = value self._val = float(1) if type(value) == Fraction: - self.numerator = value.numerator - self.denominator = value.denominator + self._numerator = value.numerator + self._denominator = value.denominator self._val = value if type(value) == IFDRational: - self.denominator = value.denominator - self.numerator = value.numerator + self._denominator = value.denominator + self._numerator = value.numerator self._val = value._val return @@ -274,6 +274,14 @@ class IFDRational(Rational): 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): """ From 8ed2d1ed02e13d57639d1fe2424e5415e9f3ec61 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 27 Dec 2015 11:27:18 +0000 Subject: [PATCH 06/12] Changing the type of the target values --- Tests/test_file_tiff_metadata.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9c976e733..f3f310d8b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -73,7 +73,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': TiffImagePlugin.IFDRational(4294967295, 113653537), 'PlanarConfiguration': 1, 'BitsPerSample': (1,), 'ImageLength': 128, @@ -83,7 +83,7 @@ class TestFileTiffMetadata(PillowTestCase): 'ResolutionUnit': 3, 'PhotometricInterpretation': 0, 'PageNumber': (0, 1), - 'XResolution': 4294967295 / 113653537, + 'XResolution': TiffImagePlugin.IFDRational(4294967295, 113653537), 'ImageWidth': 128, 'Orientation': 1, 'StripByteCounts': (1968,), @@ -125,7 +125,16 @@ class TestFileTiffMetadata(PillowTestCase): 'StripByteCounts', 'RowsPerStrip', 'PageNumber', 'StripOffsets'] for tag, value in reloaded.items(): - if tag not in ignored: + if tag in ignored: continue + if (type(original[tag]) == tuple + and type(original[tag][0]) == TiffImagePlugin.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" % From 5e7a5bf237c806ed5b05512cd82dd16a9e4e080f Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 27 Dec 2015 20:47:13 +0000 Subject: [PATCH 07/12] Limit rationals for expected values in round trip --- Tests/test_file_tiff_metadata.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index f3f310d8b..76eb35440 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -6,6 +6,7 @@ import struct from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags +from 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': TiffImagePlugin.IFDRational(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': TiffImagePlugin.IFDRational(4294967295, 113653537), + 'XResolution': IFDRational(4294967295, 113653537), 'ImageWidth': 128, 'Orientation': 1, 'StripByteCounts': (1968,), @@ -121,13 +122,21 @@ 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 in ignored: continue if (type(original[tag]) == tuple - and type(original[tag][0]) == TiffImagePlugin.IFDRational): + 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], From 3dd4b39411b666a794834317e5a92807fbf50e79 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 27 Dec 2015 20:54:14 +0000 Subject: [PATCH 08/12] Namespace --- Tests/test_file_tiff_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 76eb35440..9290d1d2b 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -6,7 +6,7 @@ import struct from helper import unittest, PillowTestCase, hopper from PIL import Image, TiffImagePlugin, TiffTags -from TiffImagePlugin import _limit_rational, IFDRational +from PIL.TiffImagePlugin import _limit_rational, IFDRational tag_ids = dict((info.name, info.value) for info in TiffTags.TAGS_V2.values()) From bd05d66c7e499f79c011992f323640187569f083 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Sun, 27 Dec 2015 21:04:23 +0000 Subject: [PATCH 09/12] Python 3.4 support for the IFDRational --- PIL/TiffImagePlugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 3cf214edd..05a2d75d4 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -313,7 +313,8 @@ class IFDRational(Rational): """ 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'] + 'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'nonzero', + 'ceil', 'floor', 'round'] print "\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a) """ @@ -342,6 +343,9 @@ class IFDRational(Rational): __le__ = _delegate('__le__') __ge__ = _delegate('__ge__') __nonzero__ = _delegate('__nonzero__') + __ceil__ = _delegate('__ceil__') + __floor__ = _delegate('__floor__') + __round__ = _delegate('__round__') From 48e4e0722e6afd4cf38ffc70d9eeea235085ff4e Mon Sep 17 00:00:00 2001 From: wiredfool Date: Mon, 28 Dec 2015 11:15:27 +0000 Subject: [PATCH 10/12] Documentation for IFDRational --- docs/handbook/image-file-formats.rst | 42 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 2b69f2414..54d9b7274 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -477,10 +477,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 @@ -510,20 +512,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** @@ -545,10 +554,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 ^^^^ From 3ac9396e8c991e7baab66187af2a35c3f4e83605 Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 29 Dec 2015 22:00:36 +0000 Subject: [PATCH 11/12] Write round trip for rationals, including nan value --- PIL/TiffImagePlugin.py | 8 +++++--- PIL/TiffTags.py | 2 +- Tests/test_file_tiff_metadata.py | 14 ++++++++++++++ Tests/test_tiff_ifdrational.py | 17 ++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 05a2d75d4..521c7c726 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -497,11 +497,13 @@ class ImageFileDirectory_v2(collections.MutableMapping): 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: diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index d8e304d87..d00164502 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -23,7 +23,7 @@ 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 {}) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9290d1d2b..1b88fca99 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -185,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() diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 05f66d5c4..5654d4c9c 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -1,7 +1,8 @@ from __future__ import print_function -from helper import PillowTestCase +from helper import PillowTestCase, hopper +from PIL import TiffImagePlugin, Image from PIL.TiffImagePlugin import IFDRational from fractions import Fraction @@ -44,3 +45,17 @@ class Test_IFDRational(PillowTestCase): 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])) + From 80ab12bdc0d6b546107536fb65e0b9c837cd09aa Mon Sep 17 00:00:00 2001 From: wiredfool Date: Tue, 29 Dec 2015 22:02:11 +0000 Subject: [PATCH 12/12] Lookup tag info in both _v2(info) and original(name only) dicts, delegate to lookup --- PIL/TiffImagePlugin.py | 12 +++++++----- PIL/TiffTags.py | 12 ++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/PIL/TiffImagePlugin.py b/PIL/TiffImagePlugin.py index 521c7c726..b502c5661 100644 --- a/PIL/TiffImagePlugin.py +++ b/PIL/TiffImagePlugin.py @@ -44,6 +44,7 @@ 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 @@ -56,7 +57,8 @@ 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. @@ -461,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): @@ -493,7 +495,7 @@ 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: @@ -648,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=" ") @@ -716,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=" ") diff --git a/PIL/TiffTags.py b/PIL/TiffTags.py index d00164502..07710fdcc 100644 --- a/PIL/TiffTags.py +++ b/PIL/TiffTags.py @@ -30,6 +30,18 @@ class TagInfo(namedtuple("_TagInfo", "value name type length enum")): 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. #