From 2ae52552f787d5916763b7fc970a27d72d612e9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 13 Apr 2022 09:54:17 +1000 Subject: [PATCH 01/32] Renamed __fp to _fp --- src/PIL/DcxImagePlugin.py | 12 ++++++------ src/PIL/FliImagePlugin.py | 14 +++++++------- src/PIL/GifImagePlugin.py | 16 ++++++++-------- src/PIL/ImImagePlugin.py | 12 ++++++------ src/PIL/Image.py | 8 ++++---- src/PIL/MicImagePlugin.py | 10 +++++----- src/PIL/MpoImagePlugin.py | 16 ++++++++-------- src/PIL/PngImagePlugin.py | 18 +++++++++--------- src/PIL/PsdImagePlugin.py | 12 ++++++------ src/PIL/SpiderImagePlugin.py | 12 ++++++------ src/PIL/TiffImagePlugin.py | 12 ++++++------ 11 files changed, 71 insertions(+), 71 deletions(-) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index de21db8f0..d5c748226 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -57,7 +57,7 @@ class DcxImageFile(PcxImageFile): break self._offset.append(offset) - self.__fp = self.fp + self._fp = self.fp self.frame = None self.n_frames = len(self._offset) self.is_animated = self.n_frames > 1 @@ -67,21 +67,21 @@ class DcxImageFile(PcxImageFile): if not self._seek_check(frame): return self.frame = frame - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self._offset[frame]) PcxImageFile._open(self) def tell(self): return self.frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None Image.register_open(DcxImageFile.format, DcxImageFile, _accept) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index ea9503305..7df301904 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -91,7 +91,7 @@ class FliImageFile(ImageFile.ImageFile): # set things up to decode first frame self.__frame = -1 - self.__fp = self.fp + self._fp = self.fp self.__rewind = self.fp.tell() self.seek(0) @@ -125,7 +125,7 @@ class FliImageFile(ImageFile.ImageFile): def _seek(self, frame): if frame == 0: self.__frame = -1 - self.__fp.seek(self.__rewind) + self._fp.seek(self.__rewind) self.__offset = 128 else: # ensure that the previous frame was loaded @@ -136,7 +136,7 @@ class FliImageFile(ImageFile.ImageFile): self.__frame = frame # move to next frame - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self.__offset) s = self.fp.read(4) @@ -153,14 +153,14 @@ class FliImageFile(ImageFile.ImageFile): def tell(self): return self.__frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index d36d8c61a..cfb6c0355 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -102,7 +102,7 @@ class GifImageFile(ImageFile.ImageFile): p = ImagePalette.raw("RGB", p) self.global_palette = self.palette = p - self.__fp = self.fp # FIXME: hack + self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() self._n_frames = None self._is_animated = None @@ -161,7 +161,7 @@ class GifImageFile(ImageFile.ImageFile): self.__offset = 0 self.dispose = None self.__frame = -1 - self.__fp.seek(self.__rewind) + self._fp.seek(self.__rewind) self.disposal_method = 0 else: # ensure that the previous frame was loaded @@ -171,7 +171,7 @@ class GifImageFile(ImageFile.ImageFile): if frame != self.__frame + 1: raise ValueError(f"cannot seek to frame {frame}") - self.fp = self.__fp + self.fp = self._fp if self.__offset: # backup to last frame self.fp.seek(self.__offset) @@ -281,7 +281,7 @@ class GifImageFile(ImageFile.ImageFile): s = None if interlace is None: - # self.__fp = None + # self._fp = None raise EOFError if not update_image: return @@ -443,14 +443,14 @@ class GifImageFile(ImageFile.ImageFile): def tell(self): return self.__frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # -------------------------------------------------------------------- diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index f7e690b35..3c5739f3d 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -245,7 +245,7 @@ class ImImageFile(ImageFile.ImageFile): self.__offset = offs = self.fp.tell() - self.__fp = self.fp # FIXME: hack + self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": @@ -294,21 +294,21 @@ class ImImageFile(ImageFile.ImageFile): size = ((self.size[0] * bits + 7) // 8) * self.size[1] offs = self.__offset + frame * size - self.fp = self.__fp + self.fp = self._fp self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))] def tell(self): return self.frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3c36178bd..16fad61c1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -544,8 +544,8 @@ class Image: def __exit__(self, *args): if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): - if hasattr(self, "_close__fp"): - self._close__fp() + if hasattr(self, "_close_fp"): + self._close_fp() if self.fp: self.fp.close() self.fp = None @@ -563,8 +563,8 @@ class Image: more information. """ try: - if hasattr(self, "_close__fp"): - self._close__fp() + if hasattr(self, "_close_fp"): + self._close_fp() if self.fp: self.fp.close() self.fp = None diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 9248b1b65..324c8eff4 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -62,7 +62,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): if not self.images: raise SyntaxError("not an MIC file; no image entries") - self.__fp = self.fp + self._fp = self.fp self.frame = None self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 @@ -89,14 +89,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 88c1bfcc5..0d61746b7 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -58,20 +58,20 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): assert self.n_frames == len(self.__mpoffsets) del self.info["mpoffset"] # no longer needed self.is_animated = self.n_frames > 1 - self.__fp = self.fp # FIXME: hack - self.__fp.seek(self.__mpoffsets[0]) # get ready to read first frame + self._fp = self.fp # FIXME: hack + self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 self.offset = 0 # for now we can only handle reading and individual frame extraction self.readonly = 1 def load_seek(self, pos): - self.__fp.seek(pos) + self._fp.seek(pos) def seek(self, frame): if not self._seek_check(frame): return - self.fp = self.__fp + self.fp = self._fp self.offset = self.__mpoffsets[frame] self.fp.seek(self.offset + 2) # skip SOI marker @@ -97,14 +97,14 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def tell(self): return self.__frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None @staticmethod def adopt(jpeg_instance, mpheader=None): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c939b86e7..313090e8d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -689,7 +689,7 @@ class PngImageFile(ImageFile.ImageFile): if not _accept(self.fp.read(8)): raise SyntaxError("not a PNG file") - self.__fp = self.fp + self._fp = self.fp self.__frame = 0 # @@ -746,7 +746,7 @@ class PngImageFile(ImageFile.ImageFile): self._close_exclusive_fp_after_loading = False self.png.save_rewind() self.__rewind_idat = self.__prepare_idat - self.__rewind = self.__fp.tell() + self.__rewind = self._fp.tell() if self.default_image: # IDAT chunk contains default image and not first animation frame self.n_frames += 1 @@ -801,7 +801,7 @@ class PngImageFile(ImageFile.ImageFile): def _seek(self, frame, rewind=False): if frame == 0: if rewind: - self.__fp.seek(self.__rewind) + self._fp.seek(self.__rewind) self.png.rewind() self.__prepare_idat = self.__rewind_idat self.im = None @@ -809,7 +809,7 @@ class PngImageFile(ImageFile.ImageFile): self.pyaccess = None self.info = self.png.im_info self.tile = self.png.im_tile - self.fp = self.__fp + self.fp = self._fp self._prev_im = None self.dispose = None self.default_image = self.info.get("default_image", False) @@ -828,7 +828,7 @@ class PngImageFile(ImageFile.ImageFile): self.im.paste(self.dispose, self.dispose_extent) self._prev_im = self.im.copy() - self.fp = self.__fp + self.fp = self._fp # advance to the next frame if self.__prepare_idat: @@ -1006,14 +1006,14 @@ class PngImageFile(ImageFile.ImageFile): else {} ) - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # -------------------------------------------------------------------- diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 283219579..3be9aa290 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -132,7 +132,7 @@ class PsdImageFile(ImageFile.ImageFile): self.tile = _maketile(self.fp, mode, (0, 0) + self.size, channels) # keep the file open - self.__fp = self.fp + self._fp = self.fp self.frame = 1 self._min_frame = 1 @@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile): self.mode = mode self.tile = tile self.frame = layer - self.fp = self.__fp + self.fp = self._fp return name, bbox except IndexError as e: raise EOFError("no such layer") from e @@ -155,14 +155,14 @@ class PsdImageFile(ImageFile.ImageFile): # return layer number (0=image, 1..max=layers) return self.frame - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None def _layerinfo(fp, ct_bytes): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1a72f5c04..0a65c286c 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.mode = "F" self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] - self.__fp = self.fp # FIXME: hack + self._fp = self.fp # FIXME: hack @property def n_frames(self): @@ -172,7 +172,7 @@ class SpiderImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) - self.fp = self.__fp + self.fp = self._fp self.fp.seek(self.stkoffset) self._open() @@ -191,14 +191,14 @@ class SpiderImageFile(ImageFile.ImageFile): return ImageTk.PhotoImage(self.convert2byte(), palette=256) - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # -------------------------------------------------------------------- diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 000429991..ee737cb59 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1073,7 +1073,7 @@ class TiffImageFile(ImageFile.ImageFile): # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 - self.__fp = self.fp + self._fp = self.fp self._frame_pos = [] self._n_frames = None @@ -1106,7 +1106,7 @@ class TiffImageFile(ImageFile.ImageFile): self.im = Image.core.new(self.mode, self.size) def _seek(self, frame): - self.fp = self.__fp + self.fp = self._fp # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer @@ -1515,14 +1515,14 @@ class TiffImageFile(ImageFile.ImageFile): self._tile_orientation = self.tag_v2.get(0x0112) - def _close__fp(self): + def _close_fp(self): try: - if self.__fp != self.fp: - self.__fp.close() + if self._fp != self.fp: + self._fp.close() except AttributeError: pass finally: - self.__fp = None + self._fp = None # From 4e075adcc5e13ffe67d25f6fab2e9b548a1d86d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 15 Apr 2022 20:31:23 +1000 Subject: [PATCH 02/32] Merged _close_fp into close and __exit__ --- src/PIL/DcxImagePlugin.py | 9 --------- src/PIL/FliImagePlugin.py | 9 --------- src/PIL/GifImagePlugin.py | 9 --------- src/PIL/ImImagePlugin.py | 9 --------- src/PIL/Image.py | 12 ++++++++---- src/PIL/MicImagePlugin.py | 9 --------- src/PIL/MpoImagePlugin.py | 9 --------- src/PIL/PngImagePlugin.py | 9 --------- src/PIL/PsdImagePlugin.py | 9 --------- src/PIL/SpiderImagePlugin.py | 9 --------- src/PIL/TiffImagePlugin.py | 9 --------- 11 files changed, 8 insertions(+), 94 deletions(-) diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index d5c748226..aeed1e7c7 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -74,15 +74,6 @@ class DcxImageFile(PcxImageFile): def tell(self): return self.frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - Image.register_open(DcxImageFile.format, DcxImageFile, _accept) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 7df301904..e13b1779c 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -153,15 +153,6 @@ class FliImageFile(ImageFile.ImageFile): def tell(self): return self.__frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # # registry diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index cfb6c0355..33c76586b 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -443,15 +443,6 @@ class GifImageFile(ImageFile.ImageFile): def tell(self): return self.__frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # -------------------------------------------------------------------- # Write GIF files diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 3c5739f3d..ee95a94cb 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -301,15 +301,6 @@ class ImImageFile(ImageFile.ImageFile): def tell(self): return self.frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 16fad61c1..1409a20d2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -544,8 +544,10 @@ class Image: def __exit__(self, *args): if hasattr(self, "fp") and getattr(self, "_exclusive_fp", False): - if hasattr(self, "_close_fp"): - self._close_fp() + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = None if self.fp: self.fp.close() self.fp = None @@ -563,8 +565,10 @@ class Image: more information. """ try: - if hasattr(self, "_close_fp"): - self._close_fp() + if getattr(self, "_fp", False): + if self._fp != self.fp: + self._fp.close() + self._fp = None if self.fp: self.fp.close() self.fp = None diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 324c8eff4..0de37cf37 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -89,15 +89,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # # -------------------------------------------------------------------- diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 0d61746b7..fc3f8556f 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -97,15 +97,6 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): def tell(self): return self.__frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - @staticmethod def adopt(jpeg_instance, mpheader=None): """ diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 313090e8d..856c21802 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1006,15 +1006,6 @@ class PngImageFile(ImageFile.ImageFile): else {} ) - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # -------------------------------------------------------------------- # PNG writer diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 3be9aa290..9622e648a 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -155,15 +155,6 @@ class PsdImageFile(ImageFile.ImageFile): # return layer number (0=image, 1..max=layers) return self.frame - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - def _layerinfo(fp, ct_bytes): # read layerinfo block diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 0a65c286c..154008c08 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -191,15 +191,6 @@ class SpiderImageFile(ImageFile.ImageFile): return ImageTk.PhotoImage(self.convert2byte(), palette=256) - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # -------------------------------------------------------------------- # Image series diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ee737cb59..c871072ad 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1515,15 +1515,6 @@ class TiffImageFile(ImageFile.ImageFile): self._tile_orientation = self.tag_v2.get(0x0112) - def _close_fp(self): - try: - if self._fp != self.fp: - self._fp.close() - except AttributeError: - pass - finally: - self._fp = None - # # -------------------------------------------------------------------- From f18688e84e1197872d2e724857573f7bd2ce4f2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 14 Apr 2022 08:28:28 +1000 Subject: [PATCH 03/32] Removed unused variable --- src/PIL/MicImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 0de37cf37..d4f6c90f7 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -62,7 +62,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): if not self.images: raise SyntaxError("not an MIC file; no image entries") - self._fp = self.fp self.frame = None self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 From e62449f94cc150584c3192061feb3cca7cd5d4c9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 17 Apr 2022 12:14:53 +1000 Subject: [PATCH 04/32] Added DeferredError to _fp --- Tests/test_file_apng.py | 9 +++++++++ Tests/test_file_fli.py | 9 +++++++++ Tests/test_file_gif.py | 13 +++++++++++++ Tests/test_file_mpo.py | 8 ++++++++ Tests/test_file_tiff.py | 9 +++++++++ src/PIL/Image.py | 4 ++-- 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index d1d5c85c1..ad61a07cc 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -637,6 +637,15 @@ def test_apng_save_blend(tmp_path): assert im.getpixel((0, 0)) == (0, 255, 0, 255) +def test_seek_after_close(): + im = Image.open("Tests/images/apng/delay.png") + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + def test_constants_deprecation(): for enum, prefix in { PngImagePlugin.Disposal: "APNG_DISPOSE_", diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index c1ad4a7f0..a7d43d2e9 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -46,6 +46,15 @@ def test_closed_file(): im.close() +def test_seek_after_close(): + im = Image.open(animated_test_file) + im.seek(1) + im.close() + + with pytest.raises(ValueError): + im.seek(0) + + def test_context_manager(): with warnings.catch_warnings(): with Image.open(static_test_file) as im: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index fd30cded0..db64dd1af 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -46,6 +46,19 @@ def test_closed_file(): im.close() +def test_seek_after_close(): + im = Image.open("Tests/images/iss634.gif") + im.load() + im.close() + + with pytest.raises(ValueError): + im.is_animated + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + + def test_context_manager(): with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index ca3ea8419..d9b59321b 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -48,6 +48,14 @@ def test_closed_file(): im.close() +def test_seek_after_close(): + im = Image.open(test_files[0]) + im.close() + + with pytest.raises(ValueError): + im.seek(1) + + def test_context_manager(): with warnings.catch_warnings(): with Image.open(test_files[0]) as im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 16c43b00f..8fdae4f13 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -70,6 +70,15 @@ class TestFileTiff: im.load() im.close() + def test_seek_after_close(self): + im = Image.open("Tests/images/multipage.tiff") + im.close() + + with pytest.raises(ValueError): + im.n_frames + with pytest.raises(ValueError): + im.seek(1) + def test_context_manager(self): with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 1409a20d2..3de2c8cd2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -547,7 +547,7 @@ class Image: if getattr(self, "_fp", False): if self._fp != self.fp: self._fp.close() - self._fp = None + self._fp = DeferredError(ValueError("Operation on closed image")) if self.fp: self.fp.close() self.fp = None @@ -568,7 +568,7 @@ class Image: if getattr(self, "_fp", False): if self._fp != self.fp: self._fp.close() - self._fp = None + self._fp = DeferredError(ValueError("Operation on closed image")) if self.fp: self.fp.close() self.fp = None From 2c5e5049db72b4f6f2c1dbf28237b9fa4f565d1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 21 Apr 2022 07:58:12 +1000 Subject: [PATCH 05/32] Ignore compression value from BMP info dictionary --- Tests/test_file_tiff.py | 7 +++++++ src/PIL/TiffImagePlugin.py | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 16c43b00f..1bdc4639a 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -690,6 +690,13 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert reloaded.info["icc_profile"] == icc_profile + def test_save_bmp_compression(self, tmp_path): + with Image.open("Tests/images/hopper.bmp") as im: + assert im.info["compression"] == 0 + + outfile = str(tmp_path / "temp.tif") + im.save(outfile) + def test_discard_icc_profile(self, tmp_path): outfile = str(tmp_path / "temp.tif") diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 000429991..3a3e5c430 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1568,7 +1568,13 @@ def _save(im, fp, filename): encoderinfo = im.encoderinfo encoderconfig = im.encoderconfig - compression = encoderinfo.get("compression", im.info.get("compression")) + try: + compression = encoderinfo["compression"] + except KeyError: + compression = im.info.get("compression") + if isinstance(compression, int): + # compression value may be from BMP. Ignore it + compression = None if compression is None: compression = "raw" elif compression == "tiff_jpeg": From a40c7a6bea437a6b6aa745c6f4752d5f62df684a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 22 Apr 2022 07:31:20 +1000 Subject: [PATCH 06/32] If font is file-like object, do not re-read from object to get variant --- Tests/test_imagefont.py | 5 ++++- src/PIL/ImageFont.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 0e1d1e637..0c50303f9 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -65,9 +65,12 @@ class TestImageFont: return font_bytes def test_font_with_filelike(self): - ImageFont.truetype( + ttf = ImageFont.truetype( self._font_as_bytes(), FONT_SIZE, layout_engine=self.LAYOUT_ENGINE ) + ttf_copy = ttf.font_variant() + assert ttf_copy.font_bytes == ttf.font_bytes + self._render(self._font_as_bytes()) # Usage note: making two fonts from the same buffer fails. # shared_bytes = self._font_as_bytes() diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 81ac03fe6..4799d71fb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -711,8 +711,13 @@ class FreeTypeFont: :return: A FreeTypeFont object. """ + if font is None: + try: + font = BytesIO(self.font_bytes) + except AttributeError: + font = self.path return FreeTypeFont( - font=self.path if font is None else font, + font=font, size=self.size if size is None else size, index=self.index if index is None else index, encoding=self.encoding if encoding is None else encoding, From b299d7cfc27a3a3365e8a01098ff830c696dc385 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 1 May 2022 22:33:49 +1000 Subject: [PATCH 07/32] Simplified code by using unsigned int instead of union --- src/libImaging/Quant.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 1c6b9d6a2..69cbcd086 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1519,7 +1519,7 @@ error_0: typedef struct { Pixel new; - Pixel furthest; + uint32_t furthestV; uint32_t furthestDistance; int secondPixel; } DistanceData; @@ -1536,7 +1536,7 @@ compute_distances(const HashTable *h, const Pixel pixel, uint32_t *dist, void *u } if (oldDist > data->furthestDistance) { data->furthestDistance = oldDist; - data->furthest.v = pixel.v; + data->furthestV = pixel.v; } } @@ -1579,8 +1579,8 @@ quantize2( data.furthestDistance = 0; data.secondPixel = (i == 1) ? 1 : 0; hashtable_foreach_update(h, compute_distances, &data); - p[i].v = data.furthest.v; - data.new.v = data.furthest.v; + p[i].v = data.furthestV; + data.new.v = data.furthestV; } hashtable_free(h); From 44494a11710e4df780ae0147f7a9ef2c0a806ade Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 1 May 2022 23:26:54 +1000 Subject: [PATCH 08/32] Set furthestV to first v in case compute_distances does not assign it --- src/libImaging/Quant.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 69cbcd086..dfa6d842d 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -1577,6 +1577,7 @@ quantize2( data.new.c.b = (int)(.5 + (double)mean[2] / (double)nPixels); for (i = 0; i < nQuantPixels; i++) { data.furthestDistance = 0; + data.furthestV = pixelData[0].v; data.secondPixel = (i == 1) ? 1 : 0; hashtable_foreach_update(h, compute_distances, &data); p[i].v = data.furthestV; From 4e12ccc63e40a9b567af3b2e1ac821f5157cddc6 Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Sat, 30 Apr 2022 22:58:44 -0700 Subject: [PATCH 09/32] Support more affine expression forms in Image.point In modes I and F, Image.point only supported affine expressions of the forms (lambda x:) x * a, x + a, and x * a + b. Expressions like 1 - x had to be written x * -1 + 1. This rewrite, though still limited to affine transformations, supports far more expression forms, including 1 - x, (2 * x + 1) / 3, etc. --- Tests/test_image_point.py | 12 ++++++-- src/PIL/Image.py | 61 +++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 428ad116b..2a4218bf8 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -18,10 +18,18 @@ def test_sanity(): im.point(lambda x: x * 1) im.point(lambda x: x + 1) im.point(lambda x: x * 1 + 1) + im.point(lambda x: 0.1 + 0.2 * x) + im.point(lambda x: -x) + im.point(lambda x: x - 0.5) + im.point(lambda x: 1 - x / 2) + im.point(lambda x: (2 + x) / 3) + im.point(lambda x: 0.5) with pytest.raises(TypeError): - im.point(lambda x: x - 1) + im.point(lambda x: x * x) with pytest.raises(TypeError): - im.point(lambda x: x / 1) + im.point(lambda x: 1 / x) + with pytest.raises(TypeError): + im.point(lambda x: x // 2) def test_16bit_lut(): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99c7ba0d1..e3a1eac70 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -431,45 +431,44 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - return value if isinstance(value, _E) else _E(value) +# _Affine(m, b) represents the polynomial m x + b +class _Affine: + def __init__(self, m, b): + self.m = m + self.b = b - -class _E: - def __init__(self, data): - self.data = data + def __neg__(self): + return _Affine(-self.m, -self.b) def __add__(self, other): - return _E((self.data, "__add__", coerce_e(other).data)) + if isinstance(other, _Affine): + return _Affine(self.m + other.m, self.b + other.b) + return _Affine(self.m, self.b + other) + + __radd__ = __add__ + + def __sub__(self, other): + return self + -other + + def __rsub__(self, other): + return other + -self def __mul__(self, other): - return _E((self.data, "__mul__", coerce_e(other).data)) + if isinstance(other, _Affine): + return NotImplemented + return _Affine(self.m * other, self.b * other) + + __rmul__ = __mul__ + + def __truediv__(self, other): + if isinstance(other, _Affine): + return NotImplemented + return _Affine(self.m / other, self.b / other) def _getscaleoffset(expr): - stub = ["stub"] - data = expr(_E(stub)).data - try: - (a, b, c) = data # simplified syntax - if a is stub and b == "__mul__" and isinstance(c, numbers.Number): - return c, 0.0 - if a is stub and b == "__add__" and isinstance(c, numbers.Number): - return 1.0, c - except TypeError: - pass - try: - ((a, b, c), d, e) = data # full syntax - if ( - a is stub - and b == "__mul__" - and isinstance(c, numbers.Number) - and d == "__add__" - and isinstance(e, numbers.Number) - ): - return c, e - except TypeError: - pass - raise ValueError("illegal expression") + a = expr(_Affine(1.0, 0.0)) + return (a.m, a.b) if isinstance(a, _Affine) else (0.0, a) # -------------------------------------------------------------------- From 46802d5def59b6694e5243e428b6419dc8a5ab43 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 3 May 2022 09:01:23 +1000 Subject: [PATCH 10/32] Removed unused import and restored existing checks (#1) * Removed unused import * Restored existing checks * Restored coerce_e, _E and data property * Deprecated coerce_e Co-authored-by: Andrew Murray --- Tests/test_image_point.py | 9 +++++++++ src/PIL/Image.py | 35 +++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 2a4218bf8..140b7a3c9 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,5 +1,7 @@ import pytest +from PIL import Image + from .helper import assert_image_equal, hopper @@ -17,6 +19,7 @@ def test_sanity(): im.point(list(range(256))) im.point(lambda x: x * 1) im.point(lambda x: x + 1) + im.point(lambda x: x - 1) im.point(lambda x: x * 1 + 1) im.point(lambda x: 0.1 + 0.2 * x) im.point(lambda x: -x) @@ -24,6 +27,7 @@ def test_sanity(): im.point(lambda x: 1 - x / 2) im.point(lambda x: (2 + x) / 3) im.point(lambda x: 0.5) + im.point(lambda x: x / 1) with pytest.raises(TypeError): im.point(lambda x: x * x) with pytest.raises(TypeError): @@ -55,3 +59,8 @@ def test_f_mode(): im = hopper("F") with pytest.raises(ValueError): im.point(None) + + +def test_coerce_e_deprecation(): + with pytest.warns(DeprecationWarning): + assert Image.coerce_e(2).data == 2 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e3a1eac70..114f4adb3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -29,7 +29,6 @@ import builtins import io import logging import math -import numbers import os import re import struct @@ -431,19 +430,23 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -# _Affine(m, b) represents the polynomial m x + b -class _Affine: - def __init__(self, m, b): - self.m = m - self.b = b +def coerce_e(value): + deprecate("coerce_e", 10) + return value if isinstance(value, _E) else _E(1, value) + + +class _E: + def __init__(self, scale, data): + self.scale = scale + self.data = data def __neg__(self): - return _Affine(-self.m, -self.b) + return _E(-self.scale, -self.data) def __add__(self, other): - if isinstance(other, _Affine): - return _Affine(self.m + other.m, self.b + other.b) - return _Affine(self.m, self.b + other) + if isinstance(other, _E): + return _E(self.scale + other.scale, self.data + other.data) + return _E(self.scale, self.data + other) __radd__ = __add__ @@ -454,21 +457,21 @@ class _Affine: return other + -self def __mul__(self, other): - if isinstance(other, _Affine): + if isinstance(other, _E): return NotImplemented - return _Affine(self.m * other, self.b * other) + return _E(self.scale * other, self.data * other) __rmul__ = __mul__ def __truediv__(self, other): - if isinstance(other, _Affine): + if isinstance(other, _E): return NotImplemented - return _Affine(self.m / other, self.b / other) + return _E(self.scale / other, self.data / other) def _getscaleoffset(expr): - a = expr(_Affine(1.0, 0.0)) - return (a.m, a.b) if isinstance(a, _Affine) else (0.0, a) + a = expr(_E(1, 0)) + return (a.scale, a.data) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- From 88f46f3c998e15ebec8b93df32316b95f7639432 Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Tue, 3 May 2022 13:42:04 -0700 Subject: [PATCH 11/32] Add a comment --- src/PIL/Image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 114f4adb3..09214e2f9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -435,6 +435,9 @@ def coerce_e(value): return value if isinstance(value, _E) else _E(1, value) +# _E(scale, offset) represents the affine transformation scale * x + offset. +# The "data" field is named for compatibility with the old implementation, +# and should be renamed once coerce_e is removed. class _E: def __init__(self, scale, data): self.scale = scale From 48f763a3785933aaed8d52a983ddae287e8f235c Mon Sep 17 00:00:00 2001 From: Ben Rudiak-Gould Date: Tue, 3 May 2022 13:53:50 -0700 Subject: [PATCH 12/32] Manually merge radarhere's additional tests --- Tests/test_image_point.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 140b7a3c9..157ecb120 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -28,8 +28,11 @@ def test_sanity(): im.point(lambda x: (2 + x) / 3) im.point(lambda x: 0.5) im.point(lambda x: x / 1) + im.point(lambda x: x + x) with pytest.raises(TypeError): im.point(lambda x: x * x) + with pytest.raises(TypeError): + im.point(lambda x: x / x) with pytest.raises(TypeError): im.point(lambda x: 1 / x) with pytest.raises(TypeError): From b3d29e946aa4ea468922b00cdc1b41ac788ef263 Mon Sep 17 00:00:00 2001 From: Ray Gardner Date: Fri, 13 May 2022 11:33:33 -0600 Subject: [PATCH 13/32] Always use GIF89a for long comments Fix bug that allows GIFs with long comments to be written as GIF87a. --- Tests/test_file_gif.py | 3 ++- src/PIL/GifImagePlugin.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3c2fab722..07b5592e8 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -804,8 +804,9 @@ def test_comment_over_255(tmp_path): im.info["comment"] = comment im.save(out) with Image.open(out) as reread: - assert reread.info["comment"] == comment + # Test that GIF89a is used for long comment + assert reread.info["version"] == b"GIF89a" def test_zero_comment_subblocks(): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9b34a3b0e..3376cccae 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -915,7 +915,7 @@ def _get_global_header(im, info): for extensionKey in ["transparency", "duration", "loop", "comment"]: if info and extensionKey in info: if (extensionKey == "duration" and info[extensionKey] == 0) or ( - extensionKey == "comment" and not (1 <= len(info[extensionKey]) <= 255) + extensionKey == "comment" and len(info[extensionKey]) == 0 ): continue version = b"89a" From 44c6467400502b69e48bbbbb0a20a70c564538fb Mon Sep 17 00:00:00 2001 From: Ray Gardner Date: Fri, 13 May 2022 11:38:39 -0600 Subject: [PATCH 14/32] Multiple GIF comments in a frame are separated If more than one comment is in a GIF frame, separate them with \r\n in the info dict. --- Tests/images/multiple_comments.gif | Bin 0 -> 1540 bytes Tests/test_file_gif.py | 6 ++++++ src/PIL/GifImagePlugin.py | 13 +++++++++---- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 Tests/images/multiple_comments.gif diff --git a/Tests/images/multiple_comments.gif b/Tests/images/multiple_comments.gif new file mode 100644 index 0000000000000000000000000000000000000000..88b2af800e89f7771c48fa9cb667097608802df0 GIT binary patch literal 1540 zcmZY8eKga190%~%R5o(^^+qKG_ROcP@hGRr((Hc!KJqRi7g z0)t9H`2P?t?onT_wMnUwpc?WA3wkkcn1K= z^xfnqJmjvVqm_%Zg&EEYql*#&0RVv9k#_Nqh(!2a#pC_MA`$34Hy8l94F(HZ0UhBc z{1Di;laW>RcunSRar0Dq(@Ym%hBjxDzm!_O3Tj^G)nOzjHjSBEPwux*NQ&K1C=IvC zg)<-dQK5@QheoO+gYN}j9jj&X`Dr2T4Y2#CIdN4jii?@7xyS>5nRZNA#C)l!-x6;d zKr6KQOePmByY-Ps?;-BUs-T~Yj3_TF+qtke+fZ3Mh@Ho0LfUGHcE$6K7trgQr#S(^ z_QpjV)?f&Y&zINfvbq#X%z(4IhMvET1uGrbcX;g^_rRHhKk~|R_=n8nrrn-lVHS+| zkHgXy+jYt(R3hB3I%WNF;WkC4r1bJxa?&IStq}x=B*iu_hJQj$!2=2ODoyoyH<<24 z%K#!W>1cWd9iq)}dAEFoYNj369$dHv8?x~TP!A2%t9WwDcP z7;HjD;`RLMo7w0h6+SfoF4YG>NK1)La~FD2u-3jC1v;wX!xCvhN4=`XI46xMcF74m zBmzt09=NN25Bh8(>NFqE$a#AsnjDj0O_0EvC6Lsp0Qp9;lMYA?3Izj9DCO3n9C2nL zv#6L|9FlBcRtPSuFRp7SKU`ZYYAFhccChLc5kR2`wynLbi(P#zz61WKv*V-zRuRG~ z6B!*d=mpPCvL5%$F20%9o>?keURi8>IPJ(=8&lHSaNPR1iQHCk|1>_Sfl@%95(O#O zY&fZO%gF8>OuhdR)d1zMh=~Fp0bh4_w?Iq4Mo6H46BBu5*fG6>$^_2RCn>6yAa?}< z_>E`j0i3)9#9u2R*x!if`gTi^)jYT!!NJEjLAigzLcJTFCY{rayThyy*Z&NvIjB-< z-j>u}N zpB0f@s%Lz^^dqo`8NQV+vsY zlM#prQvZzw(OCU$q(pWEGNwQmbTQ2pgv5-)#lV(`Yp(}FFIsn7cEVepHP=8U*6V28;QeuWRf&R z>`R(Ojx0Z(_ZiUT&SiPG{8qN+SmSMy2Y`@))9vZ&+RcEN4aB;8lR(4)R@tg;3 zvN9|*sq+JpM_dI(iuFo*W6Rz}N*$)RWT0)wCbB%F{L=gS8SmE;^G3bOx8Yb0gT0;P zs?-{>|5V0PKl{a;F>D|q@b~kO)(!O!Om$q~Gfjx9w;{^t#b`y!wS$%Z)+B|gyqp#1 z`NDC{-0GvQFXhGB^HUBEtSpmPiVm&zi#3o+iWE^pC&_Y>_M4sD5hKmPV+OCjIPXo2 HK4APe#uKGI literal 0 HcmV?d00001 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3c2fab722..9fb171411 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -813,6 +813,12 @@ def test_zero_comment_subblocks(): assert_image_equal_tofile(im, TEST_GIF) +def test_read_multiple_comments(): + with Image.open("Tests/images/multiple_comments.gif") as im: + # Multiple comments in a frame are separated not concatenated + assert im.info["comment"] == b"Test comment 1\r\nTest comment 2" + + def test_version(tmp_path): out = str(tmp_path / "temp.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 9b34a3b0e..6c2b1dedf 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -228,12 +228,17 @@ class GifImageFile(ImageFile.ImageFile): # # comment extension # + # Collect one comment block + comment = b"" while block: - if "comment" in info: - info["comment"] += block - else: - info["comment"] = block + comment += block block = self.data() + + # If multiple comments in frame, separate in info with \r\n + if "comment" in info: + info["comment"] += b"\r\n" + comment + else: + info["comment"] = comment s = None continue elif s[0] == 255: From 815839631e7262960b0369da7661de3adc1ad3ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 14 May 2022 10:11:42 +1000 Subject: [PATCH 15/32] Updated openjpeg to 2.5.0 --- depends/install_openjpeg.sh | 2 +- docs/installation.rst | 3 ++- winbuild/build_prepare.py | 10 +++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/depends/install_openjpeg.sh b/depends/install_openjpeg.sh index 914e71e53..4f4b81a62 100755 --- a/depends/install_openjpeg.sh +++ b/depends/install_openjpeg.sh @@ -1,7 +1,7 @@ #!/bin/bash # install openjpeg -archive=openjpeg-2.4.0 +archive=openjpeg-2.5.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 1807ecf9f..199a3e272 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,8 @@ Many of Pillow's features require external libraries: * **openjpeg** provides JPEG 2000 functionality. - * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1** and **2.4.0**. + * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, + **2.4.0** and **2.5.0**. * Pillow does **not** support the earlier **1.5** series which ships with Debian Jessie. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index c5fcd62ff..1c09b6002 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -246,15 +246,15 @@ deps = { "libs": [r"Lib\MS\*.lib"], }, "openjpeg": { - "url": "https://github.com/uclouvain/openjpeg/archive/v2.4.0.tar.gz", - "filename": "openjpeg-2.4.0.tar.gz", - "dir": "openjpeg-2.4.0", + "url": "https://github.com/uclouvain/openjpeg/archive/v2.5.0.tar.gz", + "filename": "openjpeg-2.5.0.tar.gz", + "dir": "openjpeg-2.5.0", "build": [ cmd_cmake(("-DBUILD_THIRDPARTY:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), cmd_nmake(target="clean"), cmd_nmake(target="openjp2"), - cmd_mkdir(r"{inc_dir}\openjpeg-2.4.0"), - cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.4.0"), + cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), + cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), ], "libs": [r"bin\*.lib"], }, From a0fa540b0b630e1c32676c200cdc17c02ef3eced Mon Sep 17 00:00:00 2001 From: Yulv-git Date: Sat, 14 May 2022 12:46:46 +0800 Subject: [PATCH 16/32] Fix some typos. --- docs/releasenotes/8.0.0.rst | 2 +- src/PIL/Image.py | 2 +- src/PIL/SpiderImagePlugin.py | 2 +- src/libImaging/GifDecode.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/releasenotes/8.0.0.rst b/docs/releasenotes/8.0.0.rst index 2ff9b3799..fe2658047 100644 --- a/docs/releasenotes/8.0.0.rst +++ b/docs/releasenotes/8.0.0.rst @@ -174,7 +174,7 @@ Previously, if a BMP file was too large, an ``OSError`` would be raised. Now, Dark theme for docs ^^^^^^^^^^^^^^^^^^^ -The https://pillow.readthedocs.io documentation will use a dark theme if the the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. +The https://pillow.readthedocs.io documentation will use a dark theme if the user has requested the system use one. Uses the ``prefers-color-scheme`` CSS media query. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c141da09f..fead48b29 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1324,7 +1324,7 @@ class Image: def getextrema(self): """ - Gets the the minimum and maximum pixel values for each band in + Gets the minimum and maximum pixel values for each band in the image. :returns: For a single-band image, a 2-tuple containing the diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1a72f5c04..d5d6f7b18 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -15,7 +15,7 @@ # ## -# Image plugin for the Spider image format. This format is is used +# Image plugin for the Spider image format. This format is used # by the SPIDER software, in processing image data from electron # microscopy and tomography. ## diff --git a/src/libImaging/GifDecode.c b/src/libImaging/GifDecode.c index 0be4771cd..92b2607b4 100644 --- a/src/libImaging/GifDecode.c +++ b/src/libImaging/GifDecode.c @@ -125,7 +125,7 @@ ImagingGifDecode(Imaging im, ImagingCodecState state, UINT8 *buffer, Py_ssize_t context->blocksize--; - /* New bits are shifted in from from the left. */ + /* New bits are shifted in from the left. */ context->bitbuffer |= (INT32)c << context->bitcount; context->bitcount += 8; From c7f5b4c2daa8882b95564127f174d94575c420d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 17 May 2022 17:31:18 +1000 Subject: [PATCH 17/32] Documented deprecation --- docs/deprecations.rst | 8 ++++++++ docs/releasenotes/9.2.0.rst | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index ad030acd0..8c5b8a748 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -170,6 +170,14 @@ in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or `PySide6 `_ instead. +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + Removed features ---------------- diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index c38944b10..db051d188 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -31,6 +31,14 @@ FreeTypeFont.getmask2 fill parameter The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been deprecated and will be removed in Pillow 10 (2023-07-01). +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 + +This undocumented method has been deprecated and will be removed in Pillow 10 +(2023-07-01). + API Changes =========== From f0353c599676d694692174e32dc3acee2912b4a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 May 2022 13:51:31 +1000 Subject: [PATCH 18/32] When reading past the end of a scan line, reduce bytes left --- Tests/images/cross_scan_line_truncated.tga | Bin 0 -> 4881 bytes Tests/test_file_tga.py | 4 ++++ src/libImaging/TgaRleDecode.c | 1 + 3 files changed, 5 insertions(+) create mode 100644 Tests/images/cross_scan_line_truncated.tga diff --git a/Tests/images/cross_scan_line_truncated.tga b/Tests/images/cross_scan_line_truncated.tga new file mode 100644 index 0000000000000000000000000000000000000000..cec4357e3acb85ebfc34e88bdbd4d4621670c456 GIT binary patch literal 4881 zcmeIzyA8k~3`NmLmu!%2$YxnRe8dDPC~}O1Tv>4CYp0w^%GIvi?HoN32>h17v-{`u m>jM->Fubuffer + state->x, ptr, n); ptr += n; + bytes -= n; extra_bytes -= n; } } From 43e2ee0433779eedc0514922af896540d7bd98d6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 May 2022 16:18:24 +1000 Subject: [PATCH 19/32] Added release notes for 9.1.1 --- CHANGES.rst | 13 +++++++++---- docs/releasenotes/9.1.1.rst | 16 ++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 docs/releasenotes/9.1.1.rst diff --git a/CHANGES.rst b/CHANGES.rst index c5bf6b5f8..b7b6fbfc6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,10 +15,6 @@ Changelog (Pillow) [radarhere] - Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 - [radarhere] - -- Do not open images with zero or negative height #6269 - [radarhere] - Search pkgconf system libs/cflags #6138 [jameshilliard, radarhere] @@ -50,6 +46,15 @@ Changelog (Pillow) - Deprecated PhotoImage.paste() box parameter #6178 [radarhere] +9.1.1 (2022-05-17) +------------------ + +- When reading past the end of a TGA scan line, reduce bytes left. CVE-2022-30595 + [radarhere] + +- Do not open images with zero or negative height #6269 + [radarhere] + 9.1.0 (2022-04-01) ------------------ diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst new file mode 100644 index 000000000..f8b155f3d --- /dev/null +++ b/docs/releasenotes/9.1.1.rst @@ -0,0 +1,16 @@ +9.1.1 +----- + +Security +======== + +This release addresses several security problems. + +:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +Pillow reads the information past the end of the first line without deducting that +from the length of the remaining file data. This vulnerability was introduced in Pillow +9.1.0, and can cause a heap buffer overflow. + +Opening an image with a zero or negative height has been found to bypass a +decompression bomb check. This will now raise a :py:exc:`SyntaxError` instead, in turn +raising a ``PIL.UnidentifiedImageError``. diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index db578bdb7..597c804f8 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -15,6 +15,7 @@ expected to be backported to earlier versions. :maxdepth: 2 9.2.0 + 9.1.1 9.1.0 9.0.1 9.0.0 From 1a286627b4fccda1ba30826c9cb2913a3860ec58 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 15 May 2022 16:15:29 +1000 Subject: [PATCH 20/32] Skip test_realloc_overflow unless libtiff 4.0.4 or higher --- Tests/test_file_libtiff.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index d83c584b5..588b9b703 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -18,6 +18,7 @@ from .helper import ( hopper, mark_if_feature_version, skip_unless_feature, + skip_unless_feature_version, ) @@ -991,6 +992,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as im: im.load() + @skip_unless_feature_version("libtiff", "4.0.4") def test_realloc_overflow(self): TiffImagePlugin.READ_LIBTIFF = True with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: From 385af47cf1baa850b77f022bb2c6bd4d4a3c8fb5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 17 May 2022 22:47:01 +0300 Subject: [PATCH 21/32] Include 'twine check' in 'make sdist' --- Makefile | 1 + RELEASING.md | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 437050ed4..ff929fee7 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,7 @@ release-test: sdist: python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build python3 -m build --sdist + python3 -m twine check --strict dist/* .PHONY: test test: diff --git a/RELEASING.md b/RELEASING.md index a6049b685..aa7511c8a 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -24,7 +24,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Create and check source distribution: ```bash make sdist - python3 -m twine check --strict dist/* ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: @@ -61,7 +60,6 @@ Released as needed for security, installation or critical bug fixes. * [ ] Create and check source distribution: ```bash make sdist - python3 -m twine check --strict dist/* ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Check and upload all binaries and source distributions e.g.: @@ -91,7 +89,6 @@ Released as needed privately to individual vendors for critical security-related * [ ] Create and check source distribution: ```bash make sdist - python3 -m twine check --strict dist/* ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) From 77aad732d162cf752f5ed267a2d92dab0ed899e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 18 May 2022 01:11:31 +0300 Subject: [PATCH 22/32] Ensure twine is installed Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index ff929fee7..219dda1de 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,7 @@ release-test: sdist: python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine python3 -m twine check --strict dist/* .PHONY: test From 69e07c2bf95de48a8f1a36476f39540129d5e850 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 18 May 2022 10:11:52 +1000 Subject: [PATCH 23/32] Upgrade non-amd64 Ubuntu jobs to Jammy --- .github/workflows/test-docker.yml | 12 ++++++------ docs/installation.rst | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index b3cfb99bb..2b4dc6b52 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -11,9 +11,9 @@ jobs: matrix: docker: [ # Run slower jobs first to give them a headstart and reduce waiting time - ubuntu-20.04-focal-arm64v8, - ubuntu-20.04-focal-ppc64le, - ubuntu-20.04-focal-s390x, + ubuntu-22.04-jammy-arm64v8, + ubuntu-22.04-jammy-ppc64le, + ubuntu-22.04-jammy-s390x, # Then run the remainder alpine, amazon-2-amd64, @@ -32,11 +32,11 @@ jobs: ] dockerTag: [main] include: - - docker: "ubuntu-20.04-focal-arm64v8" + - docker: "ubuntu-22.04-jammy-arm64v8" qemu-arch: "aarch64" - - docker: "ubuntu-20.04-focal-ppc64le" + - docker: "ubuntu-22.04-jammy-ppc64le" qemu-arch: "ppc64le" - - docker: "ubuntu-20.04-focal-s390x" + - docker: "ubuntu-22.04-jammy-s390x" qemu-arch: "s390x" name: ${{ matrix.docker }} diff --git a/docs/installation.rst b/docs/installation.rst index 199a3e272..efde6e931 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -475,11 +475,9 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | | PyPy3 | | -| +----------------------------+---------------------+ -| | 3.8 | arm64v8, ppc64le, | -| | | s390x | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| | | s390x, x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 98329354e0328d1a2206b108b3d3b150821447bc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 May 2022 20:59:16 +1000 Subject: [PATCH 24/32] Simplified version check --- src/PIL/GifImagePlugin.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 3376cccae..e8551ca33 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -912,17 +912,16 @@ def _get_global_header(im, info): # https://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp version = b"87a" - for extensionKey in ["transparency", "duration", "loop", "comment"]: - if info and extensionKey in info: - if (extensionKey == "duration" and info[extensionKey] == 0) or ( - extensionKey == "comment" and len(info[extensionKey]) == 0 - ): - continue - version = b"89a" - break - else: - if im.info.get("version") == b"89a": - version = b"89a" + if im.info.get("version") == b"89a" or ( + info + and ( + "transparency" in info + or "loop" in info + or info.get("duration") + or info.get("comment") + ) + ): + version = b"89a" background = _get_background(im, info.get("background")) From 138bd280e4768d1e7b59852c72c33ea6019e5c8e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 May 2022 20:59:32 +1000 Subject: [PATCH 25/32] Added check to test_comment as well --- Tests/test_file_gif.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 07b5592e8..a404387cb 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -794,6 +794,9 @@ def test_comment(tmp_path): with Image.open(out) as reread: assert reread.info["comment"] == im.info["comment"].encode() + # Test that GIF89a is used for comments + assert reread.info["version"] == b"GIF89a" + def test_comment_over_255(tmp_path): out = str(tmp_path / "temp.gif") @@ -805,7 +808,8 @@ def test_comment_over_255(tmp_path): im.save(out) with Image.open(out) as reread: assert reread.info["comment"] == comment - # Test that GIF89a is used for long comment + + # Test that GIF89a is used for comments assert reread.info["version"] == b"GIF89a" From 5246a179e045d7f895125ab0f5f85af5244d9e60 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 May 2022 21:07:21 +1000 Subject: [PATCH 26/32] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b7b6fbfc6..eef1a75d0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Support more affine expression forms in im.point() #6254 + [benrg, radarhere] + - Populate Python palette in fromarray() #6283 [radarhere] @@ -15,6 +18,7 @@ Changelog (Pillow) [radarhere] - Adjust BITSPERSAMPLE to match SAMPLESPERPIXEL when opening TIFFs #6270 + [radarhere] - Search pkgconf system libs/cflags #6138 [jameshilliard, radarhere] From 89f5a7d5af6bec020590778d31cde22560937056 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 20 May 2022 08:51:57 +1000 Subject: [PATCH 27/32] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index eef1a75d0..311404f12 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- If font is file-like object, do not re-read from object to get variant #6234 + [radarhere] + +- Raise ValueError when trying to access internal fp after close #6213 + [radarhere] + - Support more affine expression forms in im.point() #6254 [benrg, radarhere] From 5cb007fdf13f50d238e9bf51ad6e80aa0c78796b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 May 2022 00:07:12 +1000 Subject: [PATCH 28/32] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 311404f12..b22d29368 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Ignore compression value from BMP info dictionary when saving as TIFF #6231 + [radarhere] + - If font is file-like object, do not re-read from object to get variant #6234 [radarhere] From bf65544d2ecf4458b8ee9d920a9b009ee9dadc88 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 May 2022 10:15:00 +1000 Subject: [PATCH 29/32] Updated harfbuzz to 4.3.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1c09b6002..3d9391321 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -280,9 +280,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/4.2.1.zip", - "filename": "harfbuzz-4.2.1.zip", - "dir": "harfbuzz-4.2.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/4.3.0.zip", + "filename": "harfbuzz-4.3.0.zip", + "dir": "harfbuzz-4.3.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 91f0927e6f375ca2ad792d47fbe897daf55e76f7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 21 May 2022 10:25:19 +1000 Subject: [PATCH 30/32] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b22d29368..9127d1934 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Always use GIF89a for comments #6292 + [raygard, radarhere] + - Ignore compression value from BMP info dictionary when saving as TIFF #6231 [radarhere] From 62d5817e29b0df5dd2f86652aaa4ca4c5e415075 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 May 2022 14:11:11 +1000 Subject: [PATCH 31/32] Changed delimiter to \n --- Tests/test_file_gif.py | 6 +++--- src/PIL/GifImagePlugin.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 9fb171411..96fbfd2a1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -813,10 +813,10 @@ def test_zero_comment_subblocks(): assert_image_equal_tofile(im, TEST_GIF) -def test_read_multiple_comments(): +def test_read_multiple_comment_blocks(): with Image.open("Tests/images/multiple_comments.gif") as im: - # Multiple comments in a frame are separated not concatenated - assert im.info["comment"] == b"Test comment 1\r\nTest comment 2" + # Multiple comment blocks in a frame are separated not concatenated + assert im.info["comment"] == b"Test comment 1\nTest comment 2" def test_version(tmp_path): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6c2b1dedf..d914a0a21 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -228,15 +228,16 @@ class GifImageFile(ImageFile.ImageFile): # # comment extension # - # Collect one comment block comment = b"" + + # Collect one comment block while block: comment += block block = self.data() - # If multiple comments in frame, separate in info with \r\n if "comment" in info: - info["comment"] += b"\r\n" + comment + # If multiple comment blocks in frame, separate with \n + info["comment"] += b"\n" + comment else: info["comment"] = comment s = None From 30a0e448c181a81b435b77655d2c86549fbb4fec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 May 2022 14:54:47 +1000 Subject: [PATCH 32/32] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9127d1934..4bcd9d5ff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.2.0 (unreleased) ------------------ +- Separate multiple GIF comment blocks with newlines #6294 + [raygard, radarhere] + - Always use GIF89a for comments #6292 [raygard, radarhere]