Skip to content

Commit

Permalink
Merge pull request #3980 from kkopachev/exif-writing-fixes
Browse files Browse the repository at this point in the history
Exif writing fixes: Rational boundaries and signed/unsigned types
  • Loading branch information
radarhere committed Dec 31, 2019
2 parents f72e866 + 8924054 commit 066cc1b
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 14 deletions.
94 changes: 85 additions & 9 deletions Tests/test_file_tiff_metadata.py
Expand Up @@ -2,7 +2,7 @@
import struct

from PIL import Image, TiffImagePlugin, TiffTags
from PIL.TiffImagePlugin import IFDRational, _limit_rational
from PIL.TiffImagePlugin import IFDRational

from .helper import PillowTestCase, hopper

Expand Down Expand Up @@ -139,14 +139,6 @@ def test_write_metadata(self):
with Image.open(f) as loaded:
reloaded = loaded.tag_v2.named()

for k, v in original.items():
if isinstance(v, IFDRational):
original[k] = IFDRational(*_limit_rational(v, 2 ** 31))
elif isinstance(v, tuple) and isinstance(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():
Expand Down Expand Up @@ -225,6 +217,90 @@ def test_exif_div_zero(self):
self.assertEqual(0, reloaded.tag_v2[41988].numerator)
self.assertEqual(0, reloaded.tag_v2[41988].denominator)

def test_ifd_unsigned_rational(self):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()

max_long = 2 ** 32 - 1

# 4 bytes unsigned long
numerator = max_long

info[41493] = TiffImagePlugin.IFDRational(numerator, 1)

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(max_long, reloaded.tag_v2[41493].numerator)
self.assertEqual(1, reloaded.tag_v2[41493].denominator)

# out of bounds of 4 byte unsigned long
numerator = max_long + 1

info[41493] = TiffImagePlugin.IFDRational(numerator, 1)

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(max_long, reloaded.tag_v2[41493].numerator)
self.assertEqual(1, reloaded.tag_v2[41493].denominator)

def test_ifd_signed_rational(self):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()

# pair of 4 byte signed longs
numerator = 2 ** 31 - 1
denominator = -(2 ** 31)

info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(numerator, reloaded.tag_v2[37380].numerator)
self.assertEqual(denominator, reloaded.tag_v2[37380].denominator)

numerator = -(2 ** 31)
denominator = 2 ** 31 - 1

info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(numerator, reloaded.tag_v2[37380].numerator)
self.assertEqual(denominator, reloaded.tag_v2[37380].denominator)

# out of bounds of 4 byte signed long
numerator = -(2 ** 31) - 1
denominator = 1

info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(2 ** 31 - 1, reloaded.tag_v2[37380].numerator)
self.assertEqual(-1, reloaded.tag_v2[37380].denominator)

def test_ifd_signed_long(self):
im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2()

info[37000] = -60000

out = self.tempfile("temp.tiff")
im.save(out, tiffinfo=info, compression="raw")

reloaded = Image.open(out)
self.assertEqual(reloaded.tag_v2[37000], -60000)

def test_empty_values(self):
data = io.BytesIO(
b"II*\x00\x08\x00\x00\x00\x03\x00\x1a\x01\x05\x00\x00\x00\x00\x00"
Expand Down
35 changes: 30 additions & 5 deletions src/PIL/TiffImagePlugin.py
Expand Up @@ -263,6 +263,20 @@ def _limit_rational(val, max_val):
return n_d[::-1] if inv else n_d


def _limit_signed_rational(val, max_val, min_val):
frac = Fraction(val)
n_d = frac.numerator, frac.denominator

if min(n_d) < min_val:
n_d = _limit_rational(val, abs(min_val))

if max(n_d) > max_val:
val = Fraction(*n_d)
n_d = _limit_rational(val, max_val)

return n_d


##
# Wrapper for TIFF IFDs.

Expand Down Expand Up @@ -520,12 +534,22 @@ def _setitem(self, tag, value, legacy_api):
else:
self.tagtype[tag] = TiffTags.UNDEFINED
if all(isinstance(v, IFDRational) for v in values):
self.tagtype[tag] = TiffTags.RATIONAL
self.tagtype[tag] = (
TiffTags.RATIONAL
if all(v >= 0 for v in values)
else TiffTags.SIGNED_RATIONAL
)
elif all(isinstance(v, int) for v in values):
if all(v < 2 ** 16 for v in values):
if all(0 <= v < 2 ** 16 for v in values):
self.tagtype[tag] = TiffTags.SHORT
elif all(-(2 ** 15) < v < 2 ** 15 for v in values):
self.tagtype[tag] = TiffTags.SIGNED_SHORT
else:
self.tagtype[tag] = TiffTags.LONG
self.tagtype[tag] = (
TiffTags.LONG
if all(v >= 0 for v in values)
else TiffTags.SIGNED_LONG
)
elif all(isinstance(v, float) for v in values):
self.tagtype[tag] = TiffTags.DOUBLE
else:
Expand Down Expand Up @@ -666,7 +690,7 @@ def combine(a, b):
@_register_writer(5)
def write_rational(self, *values):
return b"".join(
self._pack("2L", *_limit_rational(frac, 2 ** 31)) for frac in values
self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values
)

@_register_loader(7, 1)
Expand All @@ -689,7 +713,8 @@ def combine(a, b):
@_register_writer(10)
def write_signed_rational(self, *values):
return b"".join(
self._pack("2L", *_limit_rational(frac, 2 ** 30)) for frac in values
self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31)))
for frac in values
)

def _ensure_read(self, fp, size):
Expand Down

0 comments on commit 066cc1b

Please sign in to comment.