diff --git a/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif new file mode 100644 index 000000000..01dca594f Binary files /dev/null and b/Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif differ diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index be00a4f9c..f676165eb 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -2,7 +2,9 @@ import logging import sys from io import BytesIO -from PIL import Image, TiffImagePlugin +import pytest + +from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError from PIL._util import py3 from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION @@ -13,7 +15,6 @@ logger = logging.getLogger(__name__) class TestFileTiff(PillowTestCase): def test_sanity(self): - filename = self.tempfile("temp.tif") hopper("RGB").save(filename) @@ -64,16 +65,14 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.mode, "RGBA") self.assertEqual(im.size, (52, 53)) - self.assertEqual( - im.tile, [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))]) + self.assertEqual(im.tile, [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))]) im.load() def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with self.assertRaises(Exception) as e: ifd.legacy_api = None - self.assertEqual(str(e.exception), - "Not allowing setting of legacy api") + self.assertEqual(str(e.exception), "Not allowing setting of legacy api") def test_size(self): filename = "Tests/images/pil168.tif" @@ -93,10 +92,8 @@ class TestFileTiff(PillowTestCase): self.assertIsInstance(im.tag[Y_RESOLUTION][0], tuple) # v2 api - self.assertIsInstance( - im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertIsInstance( - im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) self.assertEqual(im.info["dpi"], (72.0, 72.0)) @@ -105,10 +102,8 @@ class TestFileTiff(PillowTestCase): im = Image.open(filename) # v2 api - self.assertIsInstance( - im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) - self.assertIsInstance( - im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) + self.assertIsInstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) self.assertRaises(KeyError, lambda: im.tag_v2[RESOLUTION_UNIT]) # Legacy. @@ -163,12 +158,10 @@ class TestFileTiff(PillowTestCase): def test_invalid_file(self): invalid_file = "Tests/images/flower.jpg" - self.assertRaises( - SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) + self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) TiffImagePlugin.PREFIXES.append(b"\xff\xd8\xff\xe0") - self.assertRaises( - SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) + self.assertRaises(SyntaxError, TiffImagePlugin.TiffImageFile, invalid_file) TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self): @@ -223,8 +216,8 @@ class TestFileTiff(PillowTestCase): self.assertEqual(im.getpixel((0, 1)), 0) def test_12bit_rawmode(self): - """ Are we generating the same interpretation - of the image as Imagemagick is? """ + """Are we generating the same interpretation + of the image as Imagemagick is?""" im = Image.open("Tests/images/12bit.cropped.tif") @@ -243,8 +236,7 @@ class TestFileTiff(PillowTestCase): im.load() self.assertEqual(im.getpixel((0, 0)), -0.4526388943195343) - self.assertEqual( - im.getextrema(), (-3.140936851501465, 3.140684127807617)) + self.assertEqual(im.getextrema(), (-3.140936851501465, 3.140684127807617)) def test_unknown_pixel_mode(self): self.assertRaises( @@ -454,8 +446,7 @@ class TestFileTiff(PillowTestCase): self.assert_image_equal(im, im2) def test_with_underscores(self): - kwargs = {"resolution_unit": "inch", - "x_resolution": 72, "y_resolution": 36} + kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36} filename = self.tempfile("temp.tif") hopper("RGB").save(filename, **kwargs) im = Image.open(filename) @@ -486,8 +477,7 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_strip_raw.tif" im = Image.open(infile) - self.assert_image_equal_tofile( - im, "Tests/images/tiff_adobe_deflate.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_strip_planar_raw(self): # gdal_translate -of GTiff -co INTERLEAVE=BAND \ @@ -495,16 +485,14 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_strip_planar_raw.tif" im = Image.open(infile) - self.assert_image_equal_tofile( - im, "Tests/images/tiff_adobe_deflate.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_strip_planar_raw_with_overviews(self): # gdaladdo tiff_strip_planar_raw2.tif 2 4 8 16 infile = "Tests/images/tiff_strip_planar_raw_with_overviews.tif" im = Image.open(infile) - self.assert_image_equal_tofile( - im, "Tests/images/tiff_adobe_deflate.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_tiled_planar_raw(self): # gdal_translate -of GTiff -co TILED=YES -co BLOCKXSIZE=32 \ @@ -513,8 +501,7 @@ class TestFileTiff(PillowTestCase): infile = "Tests/images/tiff_tiled_planar_raw.tif" im = Image.open(infile) - self.assert_image_equal_tofile( - im, "Tests/images/tiff_adobe_deflate.png") + self.assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") def test_palette(self): for mode in ["P", "PA"]: @@ -541,8 +528,7 @@ class TestFileTiff(PillowTestCase): # Test appending images mp = io.BytesIO() im = Image.new("RGB", (100, 100), "#f00") - ims = [Image.new("RGB", (100, 100), color) - for color in ["#0f0", "#00f"]] + ims = [Image.new("RGB", (100, 100), color) for color in ["#0f0", "#00f"]] im.copy().save(mp, format="TIFF", save_all=True, append_images=ims) mp.seek(0, os.SEEK_SET) @@ -555,8 +541,7 @@ class TestFileTiff(PillowTestCase): yield im mp = io.BytesIO() - im.save(mp, format="TIFF", save_all=True, - append_images=imGenerator(ims)) + im.save(mp, format="TIFF", save_all=True, append_images=imGenerator(ims)) mp.seek(0, os.SEEK_SET) reread = Image.open(mp) @@ -616,6 +601,18 @@ class TestFileTiffW32(PillowTestCase): tmpfile = self.tempfile("temp.tif") import os + @pytest.mark.parametrize( + "test_file", + [ + "Tests/images/oom-225817ca0f8c663be7ab4b9e717b02c661e66834.tif", + ], + ) + @pytest.mark.timeout(2) + def test_oom(self, test_file): + with pytest.raises(UnidentifiedImageError): + with Image.open(test_file) as im: + im.load() + # this is an mmaped file. with Image.open("Tests/images/uint16_1_4660.tif") as im: im.save(tmpfile) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0661c2ffb..5d5df3abc 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -261,6 +261,8 @@ OPEN_INFO = { (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"), } +MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys()) + PREFIXES = [ b"MM\x00\x2A", # Valid TIFF header with big-endian byte order b"II\x2A\x00", # Valid TIFF header with little-endian byte order @@ -1264,10 +1266,25 @@ class TiffImageFile(ImageFile.ImageFile): else: bps_count = 1 bps_count += len(extra_tuple) - # Some files have only one value in bps_tuple, - # while should have more. Fix it - if bps_count > len(bps_tuple) and len(bps_tuple) == 1: - bps_tuple = bps_tuple * bps_count + bps_actual_count = len(bps_tuple) + samples_per_pixel = self.tag_v2.get( + SAMPLESPERPIXEL, + 3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1, + ) + + if samples_per_pixel > MAX_SAMPLESPERPIXEL: + # DOS check, samples_per_pixel can be a Long, and we extend the tuple below + logger.error("More samples per pixel than can be decoded: %s", samples_per_pixel) + raise SyntaxError("Invalid value for samples per pixel") + + if samples_per_pixel < bps_actual_count: + # If a file has more values in bps_tuple than expected, + # remove the excess. + bps_tuple = bps_tuple[:samples_per_pixel] + elif samples_per_pixel > bps_actual_count and bps_actual_count == 1: + # If a file has only one value in bps_tuple, when it should have more, + # presume it is the same number of bits for all of the samples. + bps_tuple = bps_tuple * samples_per_pixel # mode: check photometric interpretation and bits per pixel key = (