diff --git a/PIL/WebPImagePlugin.py b/PIL/WebPImagePlugin.py index d8d7af6eb..e15c653d2 100644 --- a/PIL/WebPImagePlugin.py +++ b/PIL/WebPImagePlugin.py @@ -7,6 +7,11 @@ _VALID_WEBP_MODES = ("RGB", "RGBA") _VP8_MODES_BY_IDENTIFIER = (b"VP8 ", b"VP8X", b"VP8L") +_INFO_CHUNKS = { + b'EXIF': 'exif', + b'ICCP': 'icc_profile', +} + def _accept(prefix): is_riff_file_format = prefix[:4] == b"RIFF" @@ -96,9 +101,8 @@ class WebPImageFile(ImageFile.ImageFile): while True: chunk_header = self.fp.read(8) - if 8 != len(chunk_header): - if first_chunk: - raise SyntaxError("not a WebP file") + if len(chunk_header) < 8: + # EOF. break chunk_fourcc = chunk_header[0:4] @@ -125,26 +129,19 @@ class WebPImageFile(ImageFile.ImageFile): elif b'ALPH' == chunk_fourcc: mode = 'RGBA' - elif b'EXIF' == chunk_fourcc: - exif = self.fp.read(chunk_size) - if chunk_size != len(exif): - raise SyntaxError("bad EXIF chunk") - self.info["exif"] = exif + elif chunk_fourcc in _INFO_CHUNKS: + data = self.fp.read(chunk_size) + if len(data) < chunk_size: + if ImageFile.LOAD_TRUNCATED_IMAGES: + # Simulate EOF. + break + msg = "image file is truncated: incomplete %s chunk" % chunk_fourcc + raise IOError(msg) + self.info[_INFO_CHUNKS[chunk_fourcc]] = data chunk_size = 0 - elif b'ICCP' == chunk_fourcc: - icc_profile = self.fp.read(chunk_size) - if chunk_size != len(icc_profile): - raise SyntaxError("bad ICCP chunk") - self.info["icc_profile"] = icc_profile - chunk_size = 0 - - if chunk_size > 0: - # Skip to next chunk. - pos = self.fp.tell() - self.fp.seek(chunk_size, os.SEEK_CUR) - if self.fp.tell() != (pos + chunk_size): - raise SyntaxError("not a WebP file") + # Skip to next chunk. + self.fp.seek(chunk_size, os.SEEK_CUR) first_chunk = False diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6c7ba98da..699e0f5ee 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,8 @@ from helper import unittest, PillowTestCase, hopper -from PIL import Image +from PIL import Image, ImageFile + +from io import BytesIO try: from PIL import _webp @@ -72,6 +74,40 @@ class TestFileWebp(PillowTestCase): target = hopper("RGB") self.assert_image_similar(image, target, 12) + def test_truncated(self): + assert False == ImageFile.LOAD_TRUNCATED_IMAGES + with open('Tests/images/flower.webp', 'rb') as fp: + full_data = fp.read() + # Truncate in the middle (VP8 chunk). + half_data = full_data[:len(full_data)//2] + im = Image.open(BytesIO(half_data)) + self.assertRaises(IOError, im.load) + im = Image.open(BytesIO(half_data)) + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im.load() + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + original = Image.open(BytesIO(full_data)) + width, height = original.size + # Check we decoded at least part of the image (top). + top_area = (0, 0, width-1, 31) + self.assert_image_equal(im.crop(top_area), original.crop(top_area)) + # Bottom should be blank. + bottom_area = (width-33, height-33, width-1, height-1) + self.assert_image_equal(im.crop(bottom_area), Image.new(original.mode, (32, 32))) + # Truncate at the end (EXIF chunk). + most_data = full_data[:-4*1024] + self.assertRaises(IOError, Image.open, BytesIO(most_data)) + ImageFile.LOAD_TRUNCATED_IMAGES = True + try: + im = Image.open(BytesIO(most_data)) + finally: + ImageFile.LOAD_TRUNCATED_IMAGES = False + im.load() + # Check we decoded the whole image. + self.assert_image_equal(im, original) + def test_info_compression(self): for name, compression in ( ('flower2' , 'lossy'),