From 6b81e34d676070e6fe519b525c27621ea6f5fe68 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 21 Dec 2020 00:56:30 -0300 Subject: [PATCH 01/94] Improve handling of PPM headers --- src/PIL/PpmImagePlugin.py | 76 +++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index abf4d651d..05993a7d1 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -20,7 +20,7 @@ from . import Image, ImageFile # # -------------------------------------------------------------------- -b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" +B_WHITESPACE = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard @@ -49,25 +49,39 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _token(self, s=b""): + def _read_token(self, token=b""): + def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF + while True: + c = self.fp.read(1) + if c in b"\r\n": + break + + while True: # read until non-whitespace is found + c = self.fp.read(1) + if c == b"#": # found comment, ignore it + _ignore_comment() + continue + if c in B_WHITESPACE: # found whitespace, ignore it + if c == b"": # reached EOF + raise EOFError("Reached EOF while reading header") + continue + break + + token += c + while True: # read until next whitespace c = self.fp.read(1) - if not c or c in b_whitespace: + if c == b"#": + _ignore_comment() + continue + if c in B_WHITESPACE: # token ended break - if c > b"\x79": - raise ValueError("Expected ASCII value, found binary") - s = s + c - if len(s) > 9: - raise ValueError("Expected int, got > 9 digits") - return s + token += c + return token def _open(self): - - # check magic - s = self.fp.read(1) - if s != b"P": - raise SyntaxError("not a PPM file") - magic_number = self._token(s) + P = self.fp.read(1) + magic_number = self._read_token(P) mode = MODES[magic_number] self.custom_mimetype = { @@ -83,29 +97,21 @@ class PpmImageFile(ImageFile.ImageFile): self.mode = rawmode = mode for ix in range(3): - while True: - while True: - s = self.fp.read(1) - if s not in b_whitespace: - break - if s == b"": - raise ValueError("File does not extend beyond magic number") - if s != b"#": - break - s = self.fp.readline() - s = int(self._token(s)) - if ix == 0: - xsize = s - elif ix == 1: - ysize = s + try: # check token sanity + token = int(self._read_token()) + except ValueError: + raise SyntaxError("Non-decimal-ASCII found in header") + if ix == 0: # token is the x size + xsize = token + elif ix == 1: # token is the y size + ysize = token if mode == "1": break - elif ix == 2: - # maxgrey - if s > 255: + elif ix == 2: # token is maxval + if token > 255: if not mode == "L": - raise ValueError(f"Too many colors for band: {s}") - if s < 2 ** 16: + raise SyntaxError(f"Too many colors for band: {token}") + if token < 2 ** 16: self.mode = "I" rawmode = "I;16B" else: From 699afe1e8915c8332e1116fbd08ef972deb9af35 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 21 Dec 2020 01:15:49 -0300 Subject: [PATCH 02/94] Improve PPM tests --- Tests/test_file_ppm.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index e7c3fb06f..9429f1e2a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,6 +1,6 @@ import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal, assert_image_similar, hopper @@ -50,12 +50,30 @@ def test_pnm(tmp_path): assert_image_equal(im, reloaded) +def test_header_with_comments(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_nondecimal_header(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n128\x00") + + with pytest.raises(UnidentifiedImageError): + Image.open(path) + + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: f.write("P6") - with pytest.raises(ValueError): + with pytest.raises(UnidentifiedImageError): Image.open(path) @@ -65,7 +83,7 @@ def test_neg_ppm(): # has been removed. The default opener doesn't accept negative # sizes. - with pytest.raises(OSError): + with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/negative_size.ppm") From d2ad27d70a00efa98560cf5036326abcae850d5e Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 4 Jan 2021 01:49:19 -0300 Subject: [PATCH 03/94] Correctly check magic number --- src/PIL/PpmImagePlugin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 05993a7d1..b9837a0fe 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,6 +49,16 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" + def _read_magic(self, s=b""): + while True: # read until next whitespace + c = self.fp.read(1) + if c in B_WHITESPACE: + break + s = s + c + if len(s) > 6: # exceeded max magic number length + break + return s + def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF while True: @@ -80,9 +90,11 @@ class PpmImageFile(ImageFile.ImageFile): return token def _open(self): - P = self.fp.read(1) - magic_number = self._read_token(P) - mode = MODES[magic_number] + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: + raise SyntaxError("Not a PPM image file") from None self.custom_mimetype = { b"P4": "image/x-portable-bitmap", From 4dbe244e42cd7225ef3b77b3d592e3dd522e4386 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:07:14 -0300 Subject: [PATCH 04/94] Add token limit --- src/PIL/PpmImagePlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index b9837a0fe..6ece914c3 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -87,6 +87,8 @@ class PpmImageFile(ImageFile.ImageFile): if c in B_WHITESPACE: # token ended break token += c + if len(token) > 9: + raise ValueError(f"Token too long: {token}") return token def _open(self): @@ -109,8 +111,9 @@ class PpmImageFile(ImageFile.ImageFile): self.mode = rawmode = mode for ix in range(3): + token = self._read_token() try: # check token sanity - token = int(self._read_token()) + token = int(token) except ValueError: raise SyntaxError("Non-decimal-ASCII found in header") if ix == 0: # token is the x size From 5d0ad5e2e9a5065d69a00890342334f374277d1c Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:15:07 -0300 Subject: [PATCH 05/94] Revert exception types to `ValueError` --- Tests/test_file_ppm.py | 6 +++--- src/PIL/PpmImagePlugin.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 9429f1e2a..21a810e30 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -64,7 +64,7 @@ def test_nondecimal_header(tmp_path): with open(path, "wb") as f: f.write(b"P6\n128\x00") - with pytest.raises(UnidentifiedImageError): + with pytest.raises(ValueError): Image.open(path) @@ -73,7 +73,7 @@ def test_truncated_file(tmp_path): with open(path, "w") as f: f.write("P6") - with pytest.raises(UnidentifiedImageError): + with pytest.raises(ValueError): Image.open(path) @@ -83,7 +83,7 @@ def test_neg_ppm(): # has been removed. The default opener doesn't accept negative # sizes. - with pytest.raises(UnidentifiedImageError): + with pytest.raises(OSError): Image.open("Tests/images/negative_size.ppm") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6ece914c3..efd845d3b 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -73,7 +73,7 @@ class PpmImageFile(ImageFile.ImageFile): continue if c in B_WHITESPACE: # found whitespace, ignore it if c == b"": # reached EOF - raise EOFError("Reached EOF while reading header") + raise ValueError("Reached EOF while reading header") continue break @@ -115,7 +115,7 @@ class PpmImageFile(ImageFile.ImageFile): try: # check token sanity token = int(token) except ValueError: - raise SyntaxError("Non-decimal-ASCII found in header") + raise ValueError(f"Non-decimal-ASCII found in header: {token}") if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size @@ -125,7 +125,7 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 2: # token is maxval if token > 255: if not mode == "L": - raise SyntaxError(f"Too many colors for band: {token}") + raise ValueError(f"Too many colors for band: {token}") if token < 2 ** 16: self.mode = "I" rawmode = "I;16B" @@ -156,7 +156,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise OSError(f"cannot write mode {im.mode} as PPM") + raise OSError(f"Cannot write mode {im.mode} as PPM") fp.write(head + ("\n%d %d\n" % im.size).encode("ascii")) if head == b"P6": fp.write(b"255\n") From 002e0bd6975974288eb46e1609ba319699dc44ba Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:21:35 -0300 Subject: [PATCH 06/94] Add tests for token size and wrong magic number --- Tests/test_file_ppm.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 21a810e30..0ea6b277e 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,6 +49,13 @@ def test_pnm(tmp_path): with Image.open(f) as reloaded: assert_image_equal(im, reloaded) +def test_not_ppm(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"PyXX") + + with pytest.raises(UnidentifiedImageError): + Image.open(path) def test_header_with_comments(tmp_path): path = str(tmp_path / "temp.ppm") @@ -67,6 +74,13 @@ def test_nondecimal_header(tmp_path): with pytest.raises(ValueError): Image.open(path) +def test_token_too_long(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n 0123456789") + + with pytest.raises(ValueError): + Image.open(path) def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") From 73fed77c0c58efd2ce11b8c0e96858bd03655163 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 14:46:30 -0300 Subject: [PATCH 07/94] Suppress exception context --- src/PIL/PpmImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index efd845d3b..8bd7d16db 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,9 @@ class PpmImageFile(ImageFile.ImageFile): try: # check token sanity token = int(token) except ValueError: - raise ValueError(f"Non-decimal-ASCII found in header: {token}") + raise ValueError( + f"Non-decimal-ASCII found in header: {token}" + ) from None if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size From bc5ecfb79c3dca0553e2b863ba7d306f2e0fd434 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 14:53:30 -0300 Subject: [PATCH 08/94] Make minor changes to tests - add test for maxcolors; - extend coverage for wrong magic number; - fix linting. --- Tests/test_file_ppm.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 0ea6b277e..77798514d 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,14 +49,16 @@ def test_pnm(tmp_path): with Image.open(f) as reloaded: assert_image_equal(im, reloaded) + def test_not_ppm(tmp_path): path = str(tmp_path / "temp.djvurle") with open(path, "wb") as f: - f.write(b"PyXX") + f.write(b"PyInvalid") with pytest.raises(UnidentifiedImageError): Image.open(path) + def test_header_with_comments(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: @@ -74,6 +76,7 @@ def test_nondecimal_header(tmp_path): with pytest.raises(ValueError): Image.open(path) + def test_token_too_long(tmp_path): path = str(tmp_path / "temp.djvurle") with open(path, "wb") as f: @@ -82,6 +85,16 @@ def test_token_too_long(tmp_path): with pytest.raises(ValueError): Image.open(path) + +def test_too_many_colors(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n1 1\n1000\n") + + with pytest.raises(ValueError): + Image.open(path) + + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: From 50522d932ea93dc09db0fae52a0ebe80a5f4eb08 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 31 Jan 2021 00:31:32 -0300 Subject: [PATCH 09/94] Change max token size to 10 - ...so as not to reject "2,147,483,647" (2 ** 31 - 1); - reword exception message. --- src/PIL/PpmImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 8bd7d16db..5ff98e346 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -87,8 +87,8 @@ class PpmImageFile(ImageFile.ImageFile): if c in B_WHITESPACE: # token ended break token += c - if len(token) > 9: - raise ValueError(f"Token too long: {token}") + if len(token) > 10: + raise ValueError(f"Token too long in file header: {token}") return token def _open(self): From 39288f0fb0755b8ef9bbc4867bda6a19e91287fd Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 31 Jan 2021 00:51:39 -0300 Subject: [PATCH 10/94] Create `maxval` variable --- src/PIL/PpmImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 5ff98e346..93ce3a4d2 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -125,7 +125,8 @@ class PpmImageFile(ImageFile.ImageFile): if mode == "1": break elif ix == 2: # token is maxval - if token > 255: + maxval = token + if maxval > 255: if not mode == "L": raise ValueError(f"Too many colors for band: {token}") if token < 2 ** 16: From b43654d15954a5af46ad618291b7c859549ccc63 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 10 Jan 2021 18:45:46 -0300 Subject: [PATCH 11/94] Change variable name in `_read_magic()` --- src/PIL/PpmImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 93ce3a4d2..e25b4bcec 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,15 +49,15 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self, s=b""): + def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) if c in B_WHITESPACE: break - s = s + c - if len(s) > 6: # exceeded max magic number length + magic += c + if len(magic) > 6: # exceeded max magic number length break - return s + return magic def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF From b6f6fba8cf170e60619da81ff1a24c2ef4364be5 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 13 Jan 2021 18:45:29 -0300 Subject: [PATCH 12/94] Rewrite `_ignore_comment` as one-liner --- src/PIL/PpmImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index e25b4bcec..95987b4a4 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -61,10 +61,8 @@ class PpmImageFile(ImageFile.ImageFile): def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF - while True: - c = self.fp.read(1) - if c in b"\r\n": - break + while self.fp.read(1) not in b"\r\n": + pass while True: # read until non-whitespace is found c = self.fp.read(1) From 41a439da7d7c38e54660011abee948d177f67126 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Mar 2021 14:42:36 +1100 Subject: [PATCH 13/94] Added context managers --- Tests/test_file_ppm.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 77798514d..94fbc62a2 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -56,7 +56,8 @@ def test_not_ppm(tmp_path): f.write(b"PyInvalid") with pytest.raises(UnidentifiedImageError): - Image.open(path) + with Image.open(path): + pass def test_header_with_comments(tmp_path): @@ -74,7 +75,8 @@ def test_nondecimal_header(tmp_path): f.write(b"P6\n128\x00") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_token_too_long(tmp_path): @@ -83,7 +85,8 @@ def test_token_too_long(tmp_path): f.write(b"P6\n 0123456789") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_too_many_colors(tmp_path): @@ -92,7 +95,8 @@ def test_too_many_colors(tmp_path): f.write(b"P6\n1 1\n1000\n") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_truncated_file(tmp_path): From 8ad5172e8858b5dd023612bbacb37f066b577012 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 21 Mar 2021 02:16:39 -0300 Subject: [PATCH 14/94] Fix wrong extension in temp test files --- Tests/test_file_ppm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 94fbc62a2..ecc3401b5 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -51,7 +51,7 @@ def test_pnm(tmp_path): def test_not_ppm(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"PyInvalid") @@ -70,7 +70,7 @@ def test_header_with_comments(tmp_path): def test_nondecimal_header(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n128\x00") @@ -80,7 +80,7 @@ def test_nondecimal_header(tmp_path): def test_token_too_long(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 0123456789") @@ -90,7 +90,7 @@ def test_token_too_long(tmp_path): def test_too_many_colors(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n1 1\n1000\n") From 9c2cbcf4520fe92d3d6b70bda13e3b1078ff73b1 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 22 Mar 2021 13:06:16 -0300 Subject: [PATCH 15/94] Keep case consistency in error messages Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 95987b4a4..725ddec10 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -157,7 +157,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise OSError(f"Cannot write mode {im.mode} as PPM") + raise OSError(f"cannot write mode {im.mode} as PPM") fp.write(head + ("\n%d %d\n" % im.size).encode("ascii")) if head == b"P6": fp.write(b"255\n") From dae1f691c2cb0afc5c5d2a05c72a3e72d9ac84a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Feb 2022 13:49:01 +1100 Subject: [PATCH 16/94] Updated checks that no warnings were raised --- Tests/test_bmp_reference.py | 9 +++------ Tests/test_file_dcx.py | 10 ++++------ Tests/test_file_fli.py | 10 ++++------ Tests/test_file_gif.py | 9 +++------ Tests/test_file_icns.py | 4 ++-- Tests/test_file_im.py | 9 +++------ Tests/test_file_jpeg.py | 4 ++-- Tests/test_file_mpo.py | 9 +++------ Tests/test_file_png.py | 4 ++-- Tests/test_file_psd.py | 10 ++++------ Tests/test_file_spider.py | 9 +++------ Tests/test_file_tar.py | 10 ++++------ Tests/test_file_tiff.py | 9 +++------ Tests/test_file_webp.py | 4 ++-- Tests/test_image.py | 4 ++-- Tests/test_imageqt.py | 6 +++--- Tests/test_numpy.py | 5 +++-- 17 files changed, 50 insertions(+), 75 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 99e16391a..440bc325b 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -1,6 +1,5 @@ import os - -import pytest +import warnings from PIL import Image @@ -20,16 +19,14 @@ def test_bad(): either""" for f in get_files("b"): - with pytest.warns(None) as record: + # Assert that there is no unclosed file warning + with warnings.catch_warnings(): try: with Image.open(f) as im: im.load() except Exception: # as msg: pass - # Assert that there is no unclosed file warning - assert not record - def test_questionable(): """These shouldn't crash/dos, but it's not well defined that these diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 58d5cbf1a..0f09c4b99 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import DcxImagePlugin, Image @@ -31,21 +33,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_invalid_file(): with open("Tests/images/flower.jpg", "rb") as fp: diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 675e06bf8..c1ad4a7f0 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import FliImagePlugin, Image @@ -38,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(static_test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(static_test_file) as im: im.load() - assert not record - def test_tell(): # Arrange diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8b0306db8..64a551f8a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -39,21 +40,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_GIF) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_GIF) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index a22fe0400..7d8f89184 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -1,5 +1,6 @@ import io import os +import warnings import pytest @@ -19,9 +20,8 @@ def test_sanity(): with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.load() - assert not record assert im.mode == "RGBA" assert im.size == (1024, 1024) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 9d25a4d1a..675210c30 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -1,4 +1,5 @@ import filecmp +import warnings import pytest @@ -35,21 +36,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_IM) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_IM) as im: im.load() - assert not record - def test_tell(): # Arrange diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f0cfb7811..e5e8c85f4 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1,5 +1,6 @@ import os import re +import warnings from io import BytesIO import pytest @@ -756,9 +757,8 @@ class TestFileJpeg: assert exif[282] == 180 out = str(tmp_path / "out.jpg") - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(out, exif=exif) - assert not record with Image.open(out) as reloaded: assert reloaded.getexif()[282] == 180 diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 9de096458..0e59e4d56 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -1,3 +1,4 @@ +import warnings from io import BytesIO import pytest @@ -41,21 +42,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_files[0]) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_files[0]) as im: im.load() - assert not record - def test_app(): for test_file in test_files: diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0869cc58b..bb2b0d119 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -1,5 +1,6 @@ import re import sys +import warnings import zlib from io import BytesIO @@ -331,9 +332,8 @@ class TestFilePng: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.verify() - assert not record with Image.open(TEST_PNG_FILE) as im: im.load() diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index a7f379e55..b4b5b7a0c 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, PsdImagePlugin @@ -29,21 +31,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(test_file) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(test_file) as im: im.load() - assert not record - def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 3c93160f1..0e3b705a2 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -1,4 +1,5 @@ import tempfile +import warnings from io import BytesIO import pytest @@ -28,21 +29,17 @@ def test_unclosed_file(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open(TEST_FILE) im.load() im.close() - assert not record - def test_context_manager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open(TEST_FILE) as im: im.load() - assert not record - def test_save(tmp_path): # Arrange diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index b38727fb9..5daab47fc 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image, TarIO, features @@ -31,16 +33,12 @@ def test_unclosed_file(): def test_close(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() - assert not record - def test_contextmanager(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass - - assert not record diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index fc4fc2a1b..28aeff075 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -1,4 +1,5 @@ import os +import warnings from io import BytesIO import pytest @@ -64,20 +65,16 @@ class TestFileTiff: pytest.warns(ResourceWarning, open) def test_closed_file(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() - assert not record - def test_context_manager(self): - with pytest.warns(None) as record: + with warnings.catch_warnings(): with Image.open("Tests/images/multipage.tiff") as im: im.load() - assert not record - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e72b4993c..55897f1eb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,7 @@ import io import re import sys +import warnings import pytest @@ -161,9 +162,8 @@ class TestFileWebp: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: temp_file = str(tmp_path / "temp.webp") - with pytest.warns(None) as record: + with warnings.catch_warnings(): image.save(temp_file) - assert not record def test_file_pointer_could_be_reused(self): file_path = "Tests/images/hopper.webp" diff --git a/Tests/test_image.py b/Tests/test_image.py index b616d06ff..2cd858df1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -3,6 +3,7 @@ import os import shutil import sys import tempfile +import warnings import pytest @@ -648,9 +649,8 @@ class TestImage: # Act/Assert with Image.open(test_file) as im: - with pytest.warns(None) as record: + with warnings.catch_warnings(): im.save(temp_file) - assert not record def test_load_on_nonexclusive_multiframe(self): with open("Tests/images/frozenpond.mpo", "rb") as fp: diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 589cb5a21..930907939 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import ImageQt @@ -56,7 +58,5 @@ def test_image(): def test_closed_file(): - with pytest.warns(None) as record: + with warnings.catch_warnings(): ImageQt.ImageQt("Tests/images/hopper.gif") - - assert not record diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 936474fe8..9735837bc 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,3 +1,5 @@ +import warnings + import pytest from PIL import Image @@ -237,6 +239,5 @@ def test_no_resource_warning_for_numpy_array(): with Image.open(test_file) as im: # Act/Assert - with pytest.warns(None) as record: + with warnings.catch_warnings(): array(im) - assert not record From 35cdcdc65d48229227c7357d755b4bd7da5e37f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Feb 2022 20:32:21 +1100 Subject: [PATCH 17/94] Added ImageShow.register examples --- src/PIL/ImageShow.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f79206921..6109f0bcf 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -25,7 +25,12 @@ _viewers = [] def register(viewer, order=1): """ - The :py:func:`register` function is used to register additional viewers. + The :py:func:`register` function is used to register additional viewers:: + + from PIL import ImageShow + ImageShow.register(MyViewer()) # MyViewer will be used as a last resort + ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised + ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised :param viewer: The viewer to be registered. :param order: From 40561c9517c1673454a0e935c834bd317bc6869f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Feb 2022 21:04:24 +1100 Subject: [PATCH 18/94] Document #6045 --- docs/reference/ImageShow.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/reference/ImageShow.rst b/docs/reference/ImageShow.rst index 45b50c846..5cedede69 100644 --- a/docs/reference/ImageShow.rst +++ b/docs/reference/ImageShow.rst @@ -23,6 +23,9 @@ All default viewers convert the image to be shown to PNG format. .. autoclass:: PIL.ImageShow.EogViewer .. autoclass:: PIL.ImageShow.XVViewer + To provide maximum functionality on Unix-based systems, temporary files created + from images will not be automatically removed by Pillow. + .. autofunction:: PIL.ImageShow.register .. autoclass:: PIL.ImageShow.Viewer :member-order: bysource From 9006836c28268a460cbf7a0c6ef3f9c46ed21875 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Feb 2022 08:55:02 +1100 Subject: [PATCH 19/94] Test that n_frames and is_animated do not change the image --- Tests/test_file_gif.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 64a551f8a..011d982f0 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -308,6 +308,22 @@ def test_n_frames(): assert im.is_animated == (n_frames != 1) +def test_no_change(): + # Test n_frames does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(1) + expected = im.copy() + assert im.n_frames == 5 + assert_image_equal(im, expected) + + # Test is_animated does not change the image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + im.seek(3) + expected = im.copy() + assert im.is_animated + assert_image_equal(im, expected) + + def test_eoferror(): with Image.open(TEST_GIF) as im: n_frames = im.n_frames From 3c1e7a7f5afd4a072ada4e14fad6f708ef082801 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Feb 2022 10:32:02 +1100 Subject: [PATCH 20/94] Corrected docstring [ci skip] --- src/PIL/ImageFont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 79e7c4b9f..f21b6de71 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -839,7 +839,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): :file:`/System/Library/Fonts/` and :file:`~/Library/Fonts/` on macOS. - :param size: The requested size, in points. + :param size: The requested size, in pixels. :param index: Which font face to load (default is first available face). :param encoding: Which font encoding to use (default is Unicode). Possible encodings include (see the FreeType documentation for more From 030a6225deba103e76d511fb1bb500f2ceb100aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Feb 2022 11:03:36 +1100 Subject: [PATCH 21/94] Removed duplicate test --- Tests/test_imagefont.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 177b18202..f9d0a4c4f 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -88,19 +88,6 @@ class TestImageFont: ImageFont.truetype(tempfile, FONT_SIZE) - def test_unavailable_layout_engine(self): - have_raqm = ImageFont.core.HAVE_RAQM - ImageFont.core.HAVE_RAQM = False - - try: - ttf = ImageFont.truetype( - FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM - ) - finally: - ImageFont.core.HAVE_RAQM = have_raqm - - assert ttf.layout_engine == ImageFont.Layout.BASIC - def _render(self, font): txt = "Hello World!" ttf = ImageFont.truetype(font, FONT_SIZE, layout_engine=self.LAYOUT_ENGINE) From 415d679f843db38fc7a0d5f5b2b1316d317873f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Feb 2022 15:26:16 +1100 Subject: [PATCH 22/94] Added Gentoo to Docker jobs --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index db866774c..2762d80c9 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -25,6 +25,7 @@ jobs: debian-11-bullseye-x86, fedora-34-amd64, fedora-35-amd64, + gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ] diff --git a/docs/installation.rst b/docs/installation.rst index 4add81352..d59f6b8e4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -465,6 +465,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 35 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Gentoo | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | From 57f106c6834f9209e99aef1cfbd1f3d8289a5e7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Feb 2022 09:16:05 +1100 Subject: [PATCH 23/94] Enable heap verification without gflags --- .github/workflows/test-windows.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index c78f9fd24..e2cf44cae 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -137,10 +137,11 @@ jobs: & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh - # failing with PyPy3 + # skip PyPy for speed - name: Enable heap verification if: "!contains(matrix.python-version, 'pypy')" - run: "& 'C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x86\\gflags.exe' /p /enable $env:pythonLocation\\python.exe" + run: | + & reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\python.exe" /v "GlobalFlag" /t REG_SZ /d "0x02000000" /f - name: Test Pillow run: | From 839b634fd5ce6eefd84e6d1f6e3e597ac4b9d2a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Feb 2022 23:28:23 +1100 Subject: [PATCH 24/94] Install gcc to allow coverage to build --- .github/workflows/test-mingw.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 8a9c1725d..51bd3a300 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,6 +45,7 @@ jobs: ${{ matrix.package }}-python-pyqt6 \ ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ + ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ ${{ matrix.package }}-lcms2 \ ${{ matrix.package }}-libimagequant \ From b14aa51037ee810b9db3eb9bc4c6c494d621a0bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Feb 2022 15:39:37 +0200 Subject: [PATCH 25/94] Replace requirements.txt with extras_require --- Makefile | 9 +-------- docs/reference/c_extension_debugging.rst | 2 +- requirements.txt | 18 ------------------ setup.cfg | 20 ++++++++++++++++++++ tox.ini | 5 ++--- 5 files changed, 24 insertions(+), 30 deletions(-) delete mode 100644 requirements.txt diff --git a/Makefile b/Makefile index 74a6a5ab2..0c018ad5d 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,6 @@ help: @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" - @echo " install-req install documentation and test dependencies" @echo " install-venv (deprecated) install in virtualenv" @echo " lint run the lint checks" @echo " lint-fix run black and isort to (mostly) fix lint issues." @@ -70,20 +69,14 @@ debug: make clean > /dev/null CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null -.PHONY: install-req -install-req: - python3 -m pip install -r requirements.txt - .PHONY: install-venv install-venv: echo "'install-venv' is deprecated and will be removed in a future Pillow release" virtualenv . - bin/pip install -r requirements.txt .PHONY: release-test release-test: - $(MAKE) install-req - python3 -m pip install -e . + python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests python3 -m pip install . diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 2ba95b8a6..dc4c2bf94 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -53,7 +53,7 @@ Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` virtualenv -p python3.8-dbg ~/vpy38-dbg source ~/vpy38-dbg/bin/activate - cd ~/Pillow && pip install -r requirements.txt && make install + cd ~/Pillow && make install Test Case --------- diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1e150e304..000000000 --- a/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Development, documentation & testing requirements. -black -check-manifest -coverage -defusedxml -markdown2 -olefile -packaging -pyroma -pytest -pytest-cov -pytest-timeout -sphinx>=2.4 -sphinx-copybutton -sphinx-issues>=3.0.1 -sphinx-removed-in -sphinx-rtd-theme>=1.0 -sphinxext-opengraph diff --git a/setup.cfg b/setup.cfg index c3b5a3197..5406716f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,26 @@ project_urls = [options] python_requires = >=3.7 +[options.extras_require] +docs = + sphinx>=2.4 + sphinx-copybutton + sphinx-issues>=3.0.1 + sphinx-removed-in + sphinx-rtd-theme>=1.0 + sphinxext-opengraph +tests = + check-manifest + coverage + defusedxml + markdown2 + olefile + packaging + pyroma + pytest + pytest-cov + pytest-timeout + [flake8] extend-ignore = E203 max-line-length = 88 diff --git a/tox.ini b/tox.ini index bdedc2bd5..09db05884 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,8 @@ envlist = minversion = 1.9 [testenv] +extras = + tests commands = make clean {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . @@ -18,9 +20,6 @@ commands = deps = cffi numpy - olefile - pyroma - pytest [testenv:lint] commands = From c4f783e099d2928c12a2b2174faf11a2204f6e19 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Feb 2022 15:48:23 +0200 Subject: [PATCH 26/94] Install for docs build using 'pip install .[docs]' --- .readthedocs.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 73e1f8213..0f581ebba 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,2 +1,8 @@ +version: 2 + python: - pip_install: true + install: + - method: pip + path: . + extra_requirements: + - docs From 3f06b0e3d59776da0265937df5612f47c10898bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Feb 2022 16:43:33 +0200 Subject: [PATCH 27/94] Docs: olefile required FpxImagePlugin and MicImagePlugin --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 5406716f7..5ca5831cf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ python_requires = >=3.7 [options.extras_require] docs = + olefile sphinx>=2.4 sphinx-copybutton sphinx-issues>=3.0.1 From 247809aa54990c714fc5ecad9de436b89981ecb2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Feb 2022 16:47:39 +0200 Subject: [PATCH 28/94] Remove non-existent commands from help --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 0c018ad5d..a7738ef0c 100644 --- a/Makefile +++ b/Makefile @@ -44,8 +44,6 @@ help: @echo " lint-fix run black and isort to (mostly) fix lint issues." @echo " release-test run code and package tests before release" @echo " test run tests on installed pillow" - @echo " upload build and upload sdists to PyPI" - @echo " upload-test build and upload sdists to test.pythonpackages.com" .PHONY: inplace inplace: clean From 8c52290b9452850dd825420b1c733df517b509fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 19 Feb 2022 16:47:59 +0200 Subject: [PATCH 29/94] Capitalisation --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a7738ef0c..a7ca1adc2 100644 --- a/Makefile +++ b/Makefile @@ -33,17 +33,17 @@ help: @echo "Welcome to Pillow development. Please use \`make \` where is one of" @echo " clean remove build products" @echo " coverage run coverage test (in progress)" - @echo " doc make html docs" - @echo " docserve run an http server on the docs directory" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" @echo " install-venv (deprecated) install in virtualenv" @echo " lint run the lint checks" - @echo " lint-fix run black and isort to (mostly) fix lint issues." + @echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " release-test run code and package tests before release" - @echo " test run tests on installed pillow" + @echo " test run tests on installed Pillow" .PHONY: inplace inplace: clean From c0e770c3c9aff2b385f84c61be3abcc7128949fd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Feb 2022 11:54:42 +1100 Subject: [PATCH 30/94] Invoke commands through python3 --- Makefile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index a7ca1adc2..f517c9b88 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,8 @@ release-test: -rm dist/*.egg -rmdir dist python3 -m pytest -qq - check-manifest - pyroma . + python3 -m check-manifest + python3 -m pyroma . $(MAKE) readme .PHONY: sdist @@ -92,7 +92,7 @@ sdist: .PHONY: test test: - pytest -qq + python3 -m pytest -qq .PHONY: valgrind valgrind: @@ -103,15 +103,15 @@ valgrind: .PHONY: readme readme: - markdown2 README.md > .long-description.html && open .long-description.html + python3 -m markdown2 README.md > .long-description.html && open .long-description.html .PHONY: lint lint: - tox --help > /dev/null || python3 -m pip install tox - tox -e lint + python3 -c "import tox" || python3 -m pip install tox + python3 -m tox -e lint .PHONY: lint-fix lint-fix: - black --target-version py37 . - isort . + python3 -m black --target-version py37 . + python3 -m isort . From 7566f8c3b2e2ab082fbfc643f6c0f736b4cf047d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Feb 2022 11:56:03 +1100 Subject: [PATCH 31/94] Ensure dependencies are installed --- Makefile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Makefile b/Makefile index f517c9b88..f5d703649 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,7 @@ sdist: .PHONY: test test: + python3 -c "import pytest" || python3 -m pip install pytest python3 -m pytest -qq .PHONY: valgrind @@ -103,6 +104,7 @@ valgrind: .PHONY: readme readme: + python3 -c "import markdown2" || python3 -m pip install markdown2 python3 -m markdown2 README.md > .long-description.html && open .long-description.html @@ -113,5 +115,7 @@ lint: .PHONY: lint-fix lint-fix: + python3 -c "import black" || python3 -m pip install black + python3 -c "import isort" || python3 -m pip install isort python3 -m black --target-version py37 . python3 -m isort . From c55d5d24018bd32fe99644bfc47fe9b9db87875b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 24 Feb 2022 17:59:19 +0200 Subject: [PATCH 32/94] Document requirements.txt -> extras_require in release notes --- docs/releasenotes/9.1.0.rst | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index f377656b3..3870774cd 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -18,8 +18,31 @@ Rather than returning a ``SystemError``, passing the incorrect types of coordina a path will now raise a more specific ``ValueError``, with the message "incorrect coordinate type". +Replace requirements.txt with extras +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Rather than installing all dependencies for docs and tests via ``requirements.txt``, +``extras_require`` is used instead. This installs only those needed and at the same +time as installing Pillow. + +For example: + +.. code-block:: bash + + # Install with dependencies for tests: + python3 -m pip install .[tests] + + # Or for building docs: + python3 -m pip install .[docs] + + # Or for all: + python3 -m pip install .[docs,tests] + +Therefore ``requirements.txt`` has been removed along with the ``make install-req`` +command for installing its contents. + Deprecations -^^^^^^^^^^^^ +============ Constants ~~~~~~~~~ @@ -87,7 +110,7 @@ Deprecated Use instead ===================================================== ============================================================ ImageShow.Viewer.show_file file argument -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by From 17d342bc4bca4b026588a3315ecf33a6b178ed5b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Feb 2022 13:04:20 +1100 Subject: [PATCH 33/94] Simplified code as both values are 7 bit --- src/libImaging/TgaRleDecode.c | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index 273ecdffd..d59766933 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -42,15 +42,13 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t return ptr - buf; } + n = depth * ((ptr[0] & 0x7f) + 1); if (ptr[0] & 0x80) { /* Run (1 + pixelsize bytes) */ - if (bytes < 1 + depth) { break; } - n = depth * ((ptr[0] & 0x7f) + 1); - if (state->x + n > state->bytes) { state->errcode = IMAGING_CODEC_OVERRUN; return -1; @@ -67,11 +65,8 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t ptr += 1 + depth; bytes -= 1 + depth; - } else { /* Literal (1+n+1 bytes block) */ - n = depth * (ptr[0] + 1); - if (bytes < 1 + n) { break; } From 0d729941a89af9e00d9d01d14ec144ab358410cd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 23 Feb 2022 15:08:33 +1100 Subject: [PATCH 34/94] Handle packets that cross scan lines --- Tests/images/cross_scan_line.png | Bin 0 -> 71 bytes Tests/images/cross_scan_line.tga | Bin 0 -> 49 bytes Tests/test_file_tga.py | 5 +++ src/libImaging/TgaRleDecode.c | 53 ++++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 Tests/images/cross_scan_line.png create mode 100644 Tests/images/cross_scan_line.tga diff --git a/Tests/images/cross_scan_line.png b/Tests/images/cross_scan_line.png new file mode 100644 index 0000000000000000000000000000000000000000..64b68ed338db798dcfbff07af1b1b96c2a0003e6 GIT binary patch literal 71 zcmeAS@N?(olHy`uVBq!ia0vp^Od!kwBpAZ)2K@k1e4Z|jAr*6yf1E#X;DbB^8w2D2 SikNMUAQhgjelF{r5}E)PRuX6c literal 0 HcmV?d00001 diff --git a/Tests/images/cross_scan_line.tga b/Tests/images/cross_scan_line.tga new file mode 100644 index 0000000000000000000000000000000000000000..5ef8c8154d4380052ca904932189383f02512d88 GIT binary patch literal 49 qcmZQz;AVgUCI%)34j}&jA11;O5)|qh<{9ki@24B#=IP_A#{d9u2nPcI literal 0 HcmV?d00001 diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index e2351d723..aeea3fb42 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -97,6 +97,11 @@ def test_id_field_rle(): assert im.size == (199, 199) +def test_cross_scan_line(): + with Image.open("Tests/images/cross_scan_line.tga") as im: + assert_image_equal_tofile(im, "Tests/images/cross_scan_line.png") + + def test_save(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: diff --git a/src/libImaging/TgaRleDecode.c b/src/libImaging/TgaRleDecode.c index d59766933..77248d145 100644 --- a/src/libImaging/TgaRleDecode.c +++ b/src/libImaging/TgaRleDecode.c @@ -20,6 +20,8 @@ int ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes) { int n, depth; UINT8 *ptr; + UINT8 extra_data = 0; + int extra_bytes = 0; ptr = buf; @@ -72,8 +74,10 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t } if (state->x + n > state->bytes) { - state->errcode = IMAGING_CODEC_OVERRUN; - return -1; + extra_bytes = n; /* full value */ + n = state->bytes - state->x; + extra_bytes -= n; + extra_data = ptr[1]; } memcpy(state->buffer + state->x, ptr + 1, n); @@ -82,24 +86,43 @@ ImagingTgaRleDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t bytes -= 1 + n; } - state->x += n; + for (;;) { + state->x += n; - if (state->x >= state->bytes) { - /* Got a full line, unpack it */ - state->shuffle( - (UINT8 *)im->image[state->y + state->yoff] + - state->xoff * im->pixelsize, - state->buffer, - state->xsize); + if (state->x >= state->bytes) { + /* Got a full line, unpack it */ + state->shuffle( + (UINT8 *)im->image[state->y + state->yoff] + + state->xoff * im->pixelsize, + state->buffer, + state->xsize); - state->x = 0; + state->x = 0; - state->y += state->ystep; + state->y += state->ystep; - if (state->y < 0 || state->y >= state->ysize) { - /* End of file (errcode = 0) */ - return -1; + if (state->y < 0 || state->y >= state->ysize) { + /* End of file (errcode = 0) */ + return -1; + } } + + if (extra_bytes == 0) { + break; + } + + if (state->x > 0) { + break; // assert + } + + if (extra_bytes >= state->bytes) { + n = state->bytes; + } else { + n = extra_bytes; + } + memcpy(state->buffer + state->x, ptr, n); + ptr += n; + extra_bytes -= n; } } From afb7728b8c6358403ce582fd8328bff06267b27f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Feb 2022 17:24:39 +1100 Subject: [PATCH 35/94] Moved unrelated tests out of TestPyDecoder --- Tests/test_imagefile.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 3e86477c5..0e3a5643b 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -124,6 +124,23 @@ class TestImageFile: with pytest.raises(OSError): p.close() + def test_no_format(self): + buf = BytesIO(b"\x00" * 255) + + class DummyImageFile(ImageFile.ImageFile): + def _open(self): + self.mode = "RGB" + self._size = (1, 1) + + im = DummyImageFile(buf) + assert im.format is None + assert im.get_format_mimetype() is None + + def test_oserror(self): + im = Image.new("RGB", (1, 1)) + with pytest.raises(OSError): + im.save(BytesIO(), "JPEG2000", num_resolutions=2) + def test_truncated(self): b = BytesIO( b"BM000000000000" # head_data @@ -258,15 +275,3 @@ class TestPyDecoder: im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] with pytest.raises(ValueError): im.load() - - def test_no_format(self): - buf = BytesIO(b"\x00" * 255) - - im = MockImageFile(buf) - assert im.format is None - assert im.get_format_mimetype() is None - - def test_oserror(self): - im = Image.new("RGB", (1, 1)) - with pytest.raises(OSError): - im.save(BytesIO(), "JPEG2000", num_resolutions=2) From dac5dfc35bd06ab340b73ae1cc94c547dcd6fc0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 10:24:29 +1100 Subject: [PATCH 36/94] Changed level of other Deprecations subheadings to be consistent --- docs/releasenotes/9.1.0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 3870774cd..a0b0b5eb7 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -45,7 +45,7 @@ Deprecations ============ Constants -~~~~~~~~~ +^^^^^^^^^ A number of constants have been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). Instead, ``enum.IntEnum`` classes have been added. @@ -121,7 +121,7 @@ In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. ``viewer.show_file(path="test.jpg")`` instead. FitsStubImagePlugin -~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^ .. deprecated:: 9.1.0 From 62a80867d67e2339b0136e8b72f611bafb1470af Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Feb 2022 10:20:15 +1100 Subject: [PATCH 37/94] Removed deprecated install-venv --- Makefile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Makefile b/Makefile index f5d703649..5eb032d88 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,6 @@ help: @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" - @echo " install-venv (deprecated) install in virtualenv" @echo " lint run the lint checks" @echo " lint-fix run Black and isort to (mostly) fix lint issues" @echo " release-test run code and package tests before release" @@ -67,11 +66,6 @@ debug: make clean > /dev/null CFLAGS='-g -O0' python3 -m pip install --global-option="build_ext" . > /dev/null -.PHONY: install-venv -install-venv: - echo "'install-venv' is deprecated and will be removed in a future Pillow release" - virtualenv . - .PHONY: release-test release-test: python3 -m pip install -e .[tests] From fa1e89ff847fd7a71620f5363b0ce7b44c1d58a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Feb 2022 11:01:05 +1100 Subject: [PATCH 38/94] Further invoking of commands through python3 --- Makefile | 4 ++-- docs/Makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 5eb032d88..604f35bd8 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,9 @@ clean: .PHONY: coverage coverage: - pytest -qq + python3 -m pytest -qq rm -r htmlcov || true - coverage report + python3 -m coverage report .PHONY: doc doc: diff --git a/docs/Makefile b/docs/Makefile index 686f0119e..549ca2c74 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +SPHINXBUILD = python3 -m sphinx.cmd.build PAPER = BUILDDIR = _build From 2e71925ee91a4e013a65d8219b1d1789405a7fdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 10:37:38 +1100 Subject: [PATCH 39/94] Further ensuring dependencies are installed --- Makefile | 14 ++++++++------ docs/Makefile | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 604f35bd8..263598599 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,10 @@ clean: .PHONY: coverage coverage: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -m pytest -qq rm -r htmlcov || true + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage python3 -m coverage report .PHONY: doc @@ -86,30 +88,30 @@ sdist: .PHONY: test test: - python3 -c "import pytest" || python3 -m pip install pytest + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest python3 -m pytest -qq .PHONY: valgrind valgrind: - python3 -c "import pytest_valgrind" || python3 -m pip install pytest-valgrind + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ --log-file=/tmp/valgrind-output \ python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output .PHONY: readme readme: - python3 -c "import markdown2" || python3 -m pip install markdown2 + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 python3 -m markdown2 README.md > .long-description.html && open .long-description.html .PHONY: lint lint: - python3 -c "import tox" || python3 -m pip install tox + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox python3 -m tox -e lint .PHONY: lint-fix lint-fix: - python3 -c "import black" || python3 -m pip install black - python3 -c "import isort" || python3 -m pip install isort + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort python3 -m black --target-version py37 . python3 -m isort . diff --git a/docs/Makefile b/docs/Makefile index 549ca2c74..0d352302f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -41,38 +41,48 @@ help: clean: -rm -rf $(BUILDDIR)/* +install-sphinx: + python3 -c "import sphinx" > /dev/null 2>&1 || python3 -m pip install sphinx + html: + $(MAKE) install-sphinx $(SPHINXBUILD) -b html -W --keep-going $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: + $(MAKE) install-sphinx $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: + $(MAKE) install-sphinx $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: + $(MAKE) install-sphinx $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ @@ -82,6 +92,7 @@ qthelp: @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PillowPILfork.qhc" devhelp: + $(MAKE) install-sphinx $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @@ -91,11 +102,13 @@ devhelp: @echo "# devhelp" epub: + $(MAKE) install-sphinx $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @@ -103,22 +116,26 @@ latex: "(use \`make latexpdf' here to do that automatically)." latexpdf: + $(MAKE) install-sphinx $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: + $(MAKE) install-sphinx $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: + $(MAKE) install-sphinx $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @@ -126,28 +143,33 @@ texinfo: "(use \`make info' here to do that automatically)." info: + $(MAKE) install-sphinx $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: + $(MAKE) install-sphinx $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: + $(MAKE) install-sphinx $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: + $(MAKE) install-sphinx $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck -j auto @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: + $(MAKE) install-sphinx $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." From a0e1fde1eddf45f26653e2ff6080d31e177adbec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 16:07:01 +1100 Subject: [PATCH 40/94] Added PyEncoder --- Tests/test_imagefile.py | 139 +++++++++++++++--- docs/handbook/appendices.rst | 2 +- ....rst => writing-your-own-image-plugin.rst} | 34 ++--- docs/reference/ImageFile.rst | 8 + src/PIL/ImageFile.py | 120 +++++++++++---- 5 files changed, 233 insertions(+), 70 deletions(-) rename docs/handbook/{writing-your-own-file-decoder.rst => writing-your-own-image-plugin.rst} (93%) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 0e3a5643b..f3da73e38 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -196,6 +196,11 @@ class MockPyDecoder(ImageFile.PyDecoder): return -1, 0 +class MockPyEncoder(ImageFile.PyEncoder): + def encode(self, buffer): + return 1, 1, b"" + + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -207,53 +212,58 @@ class MockImageFile(ImageFile.ImageFile): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder: - def get_decoder(self): - decoder = MockPyDecoder(None) +class CodecsTest: + @classmethod + def setup_class(cls): + cls.decoder = MockPyDecoder(None) + cls.encoder = MockPyEncoder(None) - def closure(mode, *args): - decoder.__init__(mode, *args) - return decoder + def decoder_closure(mode, *args): + cls.decoder.__init__(mode, *args) + return cls.decoder - Image.register_decoder("MOCK", closure) - return decoder + def encoder_closure(mode, *args): + cls.encoder.__init__(mode, *args) + return cls.encoder + Image.register_decoder("MOCK", decoder_closure) + Image.register_encoder("MOCK", encoder_closure) + + +class TestPyDecoder(CodecsTest): def test_setimage(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) - d = self.get_decoder() im.load() - assert d.state.xoff == xoff - assert d.state.yoff == yoff - assert d.state.xsize == xsize - assert d.state.ysize == ysize + assert self.decoder.state.xoff == xoff + assert self.decoder.state.yoff == yoff + assert self.decoder.state.xsize == xsize + assert self.decoder.state.ysize == ysize with pytest.raises(ValueError): - d.set_as_raw(b"\x00") + self.decoder.set_as_raw(b"\x00") def test_extents_none(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", None, 32, None)] - d = self.get_decoder() im.load() - assert d.state.xoff == 0 - assert d.state.yoff == 0 - assert d.state.xsize == 200 - assert d.state.ysize == 200 + assert self.decoder.state.xoff == 0 + assert self.decoder.state.yoff == 0 + assert self.decoder.state.xsize == 200 + assert self.decoder.state.ysize == 200 def test_negsize(self): buf = BytesIO(b"\x00" * 255) im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -267,7 +277,6 @@ class TestPyDecoder: im = MockImageFile(buf) im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)] - self.get_decoder() with pytest.raises(ValueError): im.load() @@ -275,3 +284,91 @@ class TestPyDecoder: im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)] with pytest.raises(ValueError): im.load() + + def test_decode(self): + decoder = ImageFile.PyDecoder(None) + with pytest.raises(NotImplementedError): + decoder.decode(None) + + +class TestPyEncoder(CodecsTest): + def test_setimage(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + ) + + assert self.encoder.state.xoff == xoff + assert self.encoder.state.yoff == yoff + assert self.encoder.state.xsize == xsize + assert self.encoder.state.ysize == ysize + + def test_extents_none(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + im.tile = [("MOCK", None, 32, None)] + + fp = BytesIO() + ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + + assert self.encoder.state.xoff == 0 + assert self.encoder.state.yoff == 0 + assert self.encoder.state.xsize == 200 + assert self.encoder.state.ysize == 200 + + def test_negsize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + ) + + def test_oversize(self): + buf = BytesIO(b"\x00" * 255) + + im = MockImageFile(buf) + + fp = BytesIO() + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + ) + + with pytest.raises(ValueError): + ImageFile._save( + im, + fp, + [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + ) + + def test_encode(self): + encoder = ImageFile.PyEncoder(None) + with pytest.raises(NotImplementedError): + encoder.encode(None) + + bytes_consumed, errcode = encoder.encode_to_pyfd() + assert bytes_consumed == 0 + assert ImageFile.ERRORS[errcode] == "bad configuration" + + encoder._pushes_fd = True + with pytest.raises(NotImplementedError): + encoder.encode_to_pyfd() + + with pytest.raises(NotImplementedError): + encoder.encode_to_file(None, None) diff --git a/docs/handbook/appendices.rst b/docs/handbook/appendices.rst index 6afaef071..347a8848b 100644 --- a/docs/handbook/appendices.rst +++ b/docs/handbook/appendices.rst @@ -8,4 +8,4 @@ Appendices image-file-formats text-anchors - writing-your-own-file-decoder + writing-your-own-image-plugin diff --git a/docs/handbook/writing-your-own-file-decoder.rst b/docs/handbook/writing-your-own-image-plugin.rst similarity index 93% rename from docs/handbook/writing-your-own-file-decoder.rst rename to docs/handbook/writing-your-own-image-plugin.rst index f69da9a94..0c9cfe8e8 100644 --- a/docs/handbook/writing-your-own-file-decoder.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -4,10 +4,9 @@ Writing Your Own Image Plugin ============================= Pillow uses a plugin model which allows you to add your own -decoders to the library, without any changes to the library -itself. Such plugins usually have names like -:file:`XxxImagePlugin.py`, where ``Xxx`` is a unique format name -(usually an abbreviation). +decoders and encoders to the library, without any changes to the library +itself. Such plugins usually have names like :file:`XxxImagePlugin.py`, +where ``Xxx`` is a unique format name (usually an abbreviation). .. warning:: Pillow >= 2.1.0 no longer automatically imports any file in the Python path with a name ending in @@ -413,23 +412,24 @@ value, or if there is a read error from the file. This function should free any allocated memory and release any resources from external libraries. -.. _file-decoders-py: +.. _file-codecs-py: -Writing Your Own File Decoder in Python -======================================= +Writing Your Own File Codec in Python +===================================== -Python file decoders should derive from -:py:class:`PIL.ImageFile.PyDecoder` and should at least override the -decode method. File decoders should be registered using -:py:meth:`PIL.Image.register_decoder`. As in the C implementation of -the file decoders, there are three stages in the lifetime of a -Python-based file decoder: +Python file decoders and encoders should derive from +:py:class:`PIL.ImageFile.PyDecoder` and :py:class:`PIL.ImageFile.PyEncoder` +respectively, and should at least override the decode or encode method. +They should be registered using :py:meth:`PIL.Image.register_decoder` and +:py:meth:`PIL.Image.register_encoder`. As in the C implementation of +the file codecs, there are three stages in the lifetime of a +Python-based file codec: 1. Setup: Pillow looks for the decoder in the registry, then instantiates the class. -2. Decoding: The decoder instance's ``decode`` method is repeatedly - called with a buffer of data to be interpreted. - -3. Cleanup: The decoder instance's ``cleanup`` method is called. +2. Transforming: The instance's ``decode`` method is repeatedly called with + a buffer of data to be interpreted, or the ``encode`` method is repeatedly + called with the size of data to be output. +3. Cleanup: The instance's ``cleanup`` method is called. diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index e0ce389e8..3cf59c610 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -40,8 +40,16 @@ Classes .. autoclass:: PIL.ImageFile.Parser() :members: +.. autoclass:: PIL.ImageFile.PyCodec() + :members: + .. autoclass:: PIL.ImageFile.PyDecoder() :members: + :show-inheritance: + +.. autoclass:: PIL.ImageFile.PyEncoder() + :members: + :show-inheritance: .. autoclass:: PIL.ImageFile.ImageFile() :member-order: bysource diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 331410f0e..c63cc6145 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -49,7 +49,11 @@ ERRORS = { -8: "bad configuration", -9: "out of memory error", } -"""Dict of known error codes returned from :meth:`.PyDecoder.decode`.""" +""" +Dict of known error codes returned from :meth:`.PyDecoder.decode`, +:meth:`.PyEncoder.encode` :meth:`.PyEncoder.encode_to_pyfd` and +:meth:`.PyEncoder.encode_to_file`. +""" # @@ -577,16 +581,7 @@ class PyCodecState: return (self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize) -class PyDecoder: - """ - Python implementation of a format decoder. Override this class and - add the decoding logic in the :meth:`decode` method. - - See :ref:`Writing Your Own File Decoder in Python` - """ - - _pulls_fd = False - +class PyCodec: def __init__(self, mode, *args): self.im = None self.state = PyCodecState() @@ -596,31 +591,16 @@ class PyDecoder: def init(self, args): """ - Override to perform decoder specific initialization + Override to perform codec specific initialization :param args: Array of args items from the tile entry :returns: None """ self.args = args - @property - def pulls_fd(self): - return self._pulls_fd - - def decode(self, buffer): - """ - Override to perform the decoding process. - - :param buffer: A bytes object with the data to be decoded. - :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return <0 for the bytes consumed. - Err codes are from :data:`.ImageFile.ERRORS`. - """ - raise NotImplementedError() - def cleanup(self): """ - Override to perform decoder specific cleanup + Override to perform codec specific cleanup :returns: None """ @@ -628,16 +608,16 @@ class PyDecoder: def setfd(self, fd): """ - Called from ImageFile to set the python file-like object + Called from ImageFile to set the Python file-like object - :param fd: A python file-like object + :param fd: A Python file-like object :returns: None """ self.fd = fd def setimage(self, im, extents=None): """ - Called from ImageFile to set the core output image for the decoder + Called from ImageFile to set the core output image for the codec :param im: A core image object :param extents: a 4 tuple of (x0, y0, x1, y1) defining the rectangle @@ -670,6 +650,32 @@ class PyDecoder: ): raise ValueError("Tile cannot extend outside image") + +class PyDecoder(PyCodec): + """ + Python implementation of a format decoder. Override this class and + add the decoding logic in the :meth:`decode` method. + + See :ref:`Writing Your Own File Codec in Python` + """ + + _pulls_fd = False + + @property + def pulls_fd(self): + return self._pulls_fd + + def decode(self, buffer): + """ + Override to perform the decoding process. + + :param buffer: A bytes object with the data to be decoded. + :returns: A tuple of ``(bytes consumed, errcode)``. + If finished with decoding return 0 for the bytes consumed. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + def set_as_raw(self, data, rawmode=None): """ Convenience method to set the internal image from a stream of raw data @@ -690,3 +696,55 @@ class PyDecoder: raise ValueError("not enough image data") if s[1] != 0: raise ValueError("cannot decode image data") + + +class PyEncoder(PyCodec): + """ + Python implementation of a format encoder. Override this class and + add the decoding logic in the :meth:`encode` method. + """ + + _pushes_fd = False + + @property + def pushes_fd(self): + return self._pushes_fd + + def encode(self, bufsize): + """ + Override to perform the encoding process. + + :param bufsize: Buffer size. + :returns: A tuple of ``(bytes encoded, errcode, bytes)``. + If finished with encoding return 1 for the error code. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + raise NotImplementedError() + + def encode_to_pyfd(self): + """ + :returns: A tuple of ``(bytes consumed, errcode)``. + Err codes are from :data:`.ImageFile.ERRORS`. + """ + if not self.pushes_fd: + return 0, -8 # bad configuration + bytes_consumed, errcode, data = self.encode(0) + if data: + self.fd.write(data) + return bytes_consumed, errcode + + def encode_to_file(self, fh, bufsize): + """ + :param fh: File handle. + :param bufsize: Buffer size. + + :returns: If finished successfully, return 0. + Otherwise, return an error code. Err codes are from + :data:`.ImageFile.ERRORS`. + """ + errcode = 0 + while errcode == 0: + status, errcode, buf = self.encode(bufsize) + if status > 0: + fh.write(buf[status:]) + return errcode From 747029bea9fec8dcd2234e42b31e23565b5f11c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 15:34:24 +1100 Subject: [PATCH 41/94] Simplified code --- src/PIL/BlpImagePlugin.py | 35 +++++++++++------------------------ src/encode.c | 5 ++--- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 8fd2b8510..35fd3a771 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -29,6 +29,7 @@ BLP files come in many different flavours: - DXT5 compression is used if alpha_encoding == 7. """ +import os import struct import warnings from enum import IntEnum @@ -276,7 +277,12 @@ class BlpImageFile(ImageFile.ImageFile): def _open(self): self.magic = self.fp.read(4) - self._read_blp_header() + + self.fp.seek(5, os.SEEK_CUR) + (self._blp_alpha_depth,) = struct.unpack("pushes_fd) { // UNDONE, appropriate errcode??? result = Py_BuildValue("ii", 0, IMAGING_CODEC_CONFIG); - ; return result; } @@ -307,7 +306,7 @@ static struct PyMethodDef methods[] = { {"encode", (PyCFunction)_encode, METH_VARARGS}, {"cleanup", (PyCFunction)_encode_cleanup, METH_VARARGS}, {"encode_to_file", (PyCFunction)_encode_to_file, METH_VARARGS}, - {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_VARARGS}, + {"encode_to_pyfd", (PyCFunction)_encode_to_pyfd, METH_NOARGS}, {"setimage", (PyCFunction)_setimage, METH_VARARGS}, {"setfd", (PyCFunction)_setfd, METH_VARARGS}, {NULL, NULL} /* sentinel */ From 169025df6c557874473037972dc4615bc38e9661 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 16:53:53 +1100 Subject: [PATCH 42/94] Added BLP saving --- Tests/test_file_blp.py | 15 ++++++++++- src/PIL/BlpImagePlugin.py | 54 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index c2f8d08cb..0891d4053 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -2,7 +2,7 @@ import pytest from PIL import BlpImagePlugin, Image -from .helper import assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile, hopper def test_load_blp1(): @@ -25,6 +25,19 @@ def test_load_blp2_dxt1a(): assert_image_equal_tofile(im, "Tests/images/blp/blp2_dxt1a.png") +def test_save(tmp_path): + im = hopper("P") + f = str(tmp_path / "temp.blp") + im.save(f) + + with Image.open(f) as reloaded: + assert_image_equal(im.convert("RGB"), reloaded) + + im = hopper() + with pytest.raises(ValueError): + im.save(f) + + @pytest.mark.parametrize( "test_file", [ diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 35fd3a771..dfd651867 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -267,6 +267,10 @@ class BLPFormatError(NotImplementedError): pass +def _accept(prefix): + return prefix[:4] in (b"BLP1", b"BLP2") + + class BlpImageFile(ImageFile.ImageFile): """ Blizzard Mipmap Format @@ -304,7 +308,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self._read_blp_header() self._load() except struct.error as e: - raise OSError("Truncated Blp file") from e + raise OSError("Truncated BLP file") from e return 0, 0 def _safe_read(self, length): @@ -439,12 +443,54 @@ class BLP2Decoder(_BLPBaseDecoder): self.set_as_raw(bytes(data)) -def _accept(prefix): - return prefix[:4] in (b"BLP1", b"BLP2") +class BLP2Encoder(ImageFile.PyEncoder): + _pushes_fd = True + + def _write_palette(self): + data = b"" + palette = self.im.getpalette("RGBA", "RGBA") + for i in range(256): + r, g, b, a = palette[i * 4 : (i + 1) * 4] + data += struct.pack("<4B", b, g, r, a) + return data + + def encode(self, bufsize): + palette_data = self._write_palette() + + offset = 20 + 16 * 4 * 2 + len(palette_data) + data = struct.pack("<16I", offset, *((0,) * 15)) + + w, h = self.im.size + data += struct.pack("<16I", w * h, *((0,) * 15)) + + data += palette_data + + for y in range(h): + for x in range(w): + data += struct.pack(" Date: Fri, 25 Feb 2022 16:54:53 +1100 Subject: [PATCH 43/94] Fixed reading uncompressed BLP2 with alpha --- Tests/test_file_blp.py | 14 +++++++++++++- src/PIL/BlpImagePlugin.py | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 0891d4053..86f208729 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -2,7 +2,12 @@ import pytest from PIL import BlpImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import ( + assert_image_equal, + assert_image_equal_tofile, + assert_image_similar, + hopper, +) def test_load_blp1(): @@ -33,6 +38,13 @@ def test_save(tmp_path): with Image.open(f) as reloaded: assert_image_equal(im.convert("RGB"), reloaded) + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) + im = hopper() with pytest.raises(ValueError): im.save(f) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index dfd651867..f83f1979e 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -406,7 +406,10 @@ class BLP2Decoder(_BLPBaseDecoder): except struct.error: break b, g, r, a = palette[offset] - data.extend((r, g, b)) + d = (r, g, b) + if self._blp_alpha_depth: + d += (a,) + data.extend(d) elif self._blp_encoding == Encoding.DXT: if self._blp_alpha_encoding == AlphaEncoding.DXT1: From 1859bc346260f9791d4b4c240f91aa9bf13cedac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Feb 2022 17:50:21 +1100 Subject: [PATCH 44/94] Added reading non-JPEG BLP1 as RGBA --- src/PIL/BlpImagePlugin.py | 75 ++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index f83f1979e..ffd0412e9 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -288,15 +288,12 @@ class BlpImageFile(ImageFile.ImageFile): self.fp.seek(2, os.SEEK_CUR) self._size = struct.unpack(" Date: Fri, 25 Feb 2022 23:58:13 +1100 Subject: [PATCH 45/94] Added BLP1 saving --- Tests/test_file_blp.py | 20 +++++++++++--------- docs/handbook/image-file-formats.rst | 21 ++++++++++++++------- src/PIL/BlpImagePlugin.py | 13 +++++++++---- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index 86f208729..c1fae44ca 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -31,19 +31,21 @@ def test_load_blp2_dxt1a(): def test_save(tmp_path): - im = hopper("P") f = str(tmp_path / "temp.blp") - im.save(f) - with Image.open(f) as reloaded: - assert_image_equal(im.convert("RGB"), reloaded) - - with Image.open("Tests/images/transparent.png") as im: - f = str(tmp_path / "temp.blp") - im.convert("P").save(f) + for version in ("BLP1", "BLP2"): + im = hopper("P") + im.save(f, blp_version=version) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded, 8) + assert_image_equal(im.convert("RGB"), reloaded) + + with Image.open("Tests/images/transparent.png") as im: + f = str(tmp_path / "temp.blp") + im.convert("P").save(f, blp_version=version) + + with Image.open(f) as reloaded: + assert_image_similar(im, reloaded, 8) im = hopper() with pytest.raises(ValueError): diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d1a5ba339..17808dbc4 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -26,6 +26,20 @@ Fully supported formats .. contents:: +BLP +^^^ + +BLP is the Blizzard Mipmap Format, a texture format used in World of +Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` +images, and all types of ``BLP2`` images. + +Pillow supports writing BLP images. The :py:meth:`~PIL.Image.Image.save` method +can take the following keyword arguments: + +**blp_version** + If present and set to "BLP1", images will be saved as BLP1. Otherwise, images + will be saved as BLP2. + BMP ^^^ @@ -1042,13 +1056,6 @@ Pillow reads and writes X bitmap files (mode ``1``). Read-only formats ----------------- -BLP -^^^ - -BLP is the Blizzard Mipmap Format, a texture format used in World of -Warcraft. Pillow supports reading ``JPEG`` Compressed or raw ``BLP1`` -images, and all types of ``BLP2`` images. - CUR ^^^ diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index ffd0412e9..779fddea8 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -439,7 +439,7 @@ class BLP2Decoder(_BLPBaseDecoder): self.set_as_raw(bytes(data)) -class BLP2Encoder(ImageFile.PyEncoder): +class BLPEncoder(ImageFile.PyEncoder): _pushes_fd = True def _write_palette(self): @@ -472,15 +472,20 @@ def _save(im, fp, filename, save_all=False): if im.mode != "P": raise ValueError("Unsupported BLP image mode") - fp.write(b"BLP2") + magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2" + fp.write(magic) + fp.write(struct.pack(" Date: Sat, 26 Feb 2022 15:25:50 +1100 Subject: [PATCH 46/94] Updated libjpeg-turbo to 2.1.3 --- 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 ade620347..7aa46274b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -107,9 +107,9 @@ header = [ # dependencies, listed in order of compilation deps = { "libjpeg": { - "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.2/libjpeg-turbo-2.1.2.tar.gz", - "filename": "libjpeg-turbo-2.1.2.tar.gz", - "dir": "libjpeg-turbo-2.1.2", + "url": SF_MIRROR + "/project/libjpeg-turbo/2.1.3/libjpeg-turbo-2.1.3.tar.gz", + "filename": "libjpeg-turbo-2.1.3.tar.gz", + "dir": "libjpeg-turbo-2.1.3", "build": [ cmd_cmake( [ From fbaaf3c19b0b27206205c7ce95e95db1190f227e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Feb 2022 16:30:44 +1100 Subject: [PATCH 47/94] Do not read data until necessary --- src/PIL/GbrImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 3d8fc47b2..4caeda8ef 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -43,9 +43,9 @@ class GbrImageFile(ImageFile.ImageFile): def _open(self): header_size = i32(self.fp.read(4)) - version = i32(self.fp.read(4)) if header_size < 20: raise SyntaxError("not a GIMP brush") + version = i32(self.fp.read(4)) if version not in (1, 2): raise SyntaxError(f"Unsupported GIMP brush version: {version}") From efb9d503a755499fb9c705a5d05952e6f9398c43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Feb 2022 17:29:21 +1100 Subject: [PATCH 48/94] Raise SyntaxError if data is not as expected --- Tests/test_file_xbm.py | 9 ++++++++- src/PIL/XbmImagePlugin.py | 17 +++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 487920a92..9c54c6755 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import Image +from PIL import Image, XbmImagePlugin from .helper import hopper @@ -63,6 +63,13 @@ def test_open_filename_with_underscore(): assert im.size == (128, 128) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + XbmImagePlugin.XbmImageFile(invalid_file) + + def test_save_wrong_mode(tmp_path): im = hopper() out = str(tmp_path / "temp.xbm") diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 644cfb39b..8aea0d60c 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -52,18 +52,19 @@ class XbmImageFile(ImageFile.ImageFile): m = xbm_head.match(self.fp.read(512)) - if m: + if not m: + raise SyntaxError("not a XBM file") - xsize = int(m.group("width")) - ysize = int(m.group("height")) + xsize = int(m.group("width")) + ysize = int(m.group("height")) - if m.group("hotspot"): - self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) + if m.group("hotspot"): + self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - self.mode = "1" - self._size = xsize, ysize + self.mode = "1" + self._size = xsize, ysize - self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] + self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] def _save(im, fp, filename): From 83b9e66133b0a2c105c92e682921f1de3b619c9b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Feb 2022 17:34:13 +1100 Subject: [PATCH 49/94] Moved flags check into _accept --- src/PIL/FliImagePlugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index f2d4857f7..ea9503305 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -26,7 +26,11 @@ from ._binary import o8 def _accept(prefix): - return len(prefix) >= 6 and i16(prefix, 4) in [0xAF11, 0xAF12] + return ( + len(prefix) >= 6 + and i16(prefix, 4) in [0xAF11, 0xAF12] + and i16(prefix, 14) in [0, 3] # flags + ) ## @@ -44,11 +48,7 @@ class FliImageFile(ImageFile.ImageFile): # HEAD s = self.fp.read(128) - if not ( - _accept(s) - and i16(s, 14) in [0, 3] # flags - and s[20:22] == b"\x00\x00" # reserved - ): + if not (_accept(s) and s[20:22] == b"\x00\x00"): raise SyntaxError("not an FLI/FLC file") # frames From 885e305fe97761fdfc7b3cab414ff995af76d605 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Feb 2022 17:34:29 +1100 Subject: [PATCH 50/94] Make code clearer by matching _accept condition --- src/PIL/WmfImagePlugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index c32cc52f8..2f54cdebb 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -21,7 +21,6 @@ from . import Image, ImageFile from ._binary import i16le as word -from ._binary import i32le as dword from ._binary import si16le as short from ._binary import si32le as _long @@ -112,7 +111,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): if s[22:26] != b"\x01\x00\t\x00": raise SyntaxError("Unsupported WMF file format") - elif dword(s) == 1 and s[40:44] == b" EMF": + elif s[:4] == b"\x01\x00\x00\x00" and s[40:44] == b" EMF": # enhanced metafile # get bounding box From e2288356caeb012d81e846b7ef9b6a29737dc037 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 27 Feb 2022 14:47:07 +1100 Subject: [PATCH 51/94] Use _accept check in _open --- Tests/test_file_dds.py | 7 +++++++ Tests/test_file_ftex.py | 7 +++++++ docs/example/DdsImagePlugin.py | 4 +++- src/PIL/DdsImagePlugin.py | 4 +++- src/PIL/FtexImagePlugin.py | 3 ++- 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 2f46ed77e..58447122e 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -196,6 +196,13 @@ def test__accept_false(): assert not output +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + DdsImagePlugin.DdsImageFile(invalid_file) + + def test_short_header(): """Check a short header""" with open(TEST_FILE_DXT5, "rb") as f: diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index 5447dc740..cae20fa46 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -16,6 +16,13 @@ def test_load_dxt1(): assert_image_similar(im, target.convert("RGBA"), 15) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + FtexImagePlugin.FtexImageFile(invalid_file) + + def test_constants_deprecation(): for enum, prefix in { FtexImagePlugin.Format: "FORMAT_", diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 272409416..29fefba16 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -210,7 +210,9 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self): - magic, header_size = struct.unpack(" Date: Sat, 26 Feb 2022 17:53:27 +1100 Subject: [PATCH 52/94] Simplify code by using _accept --- src/PIL/IcnsImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 069aff96b..c22bfe0ed 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -167,7 +167,7 @@ class IcnsFile: self.dct = dct = {} self.fobj = fobj sig, filesize = nextheader(fobj) - if sig != MAGIC: + if not _accept(sig): raise SyntaxError("not an icns file") i = HEADERSIZE while i < filesize: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 35ff1c1bb..d8dc1741a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -493,7 +493,7 @@ class ImageFileDirectory_v2(MutableMapping): endianness. :param prefix: Override the endianness of the file. """ - if ifh[:4] not in PREFIXES: + if not _accept(ifh): raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)") self._prefix = prefix if prefix is not None else ifh[:2] if self._prefix == MM: From 00563327a28bd051f4ee7354cb04b23625b5c88b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 27 Feb 2022 18:43:06 +0200 Subject: [PATCH 53/94] Quotes may be needed on macOS Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/releasenotes/9.1.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index a0b0b5eb7..aa4b6af06 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -38,6 +38,9 @@ For example: # Or for all: python3 -m pip install .[docs,tests] +On macOS, the last argument may need to be wrapped in quotes, e.g. +``python3 -m pip install ".[tests]"`` + Therefore ``requirements.txt`` has been removed along with the ``make install-req`` command for installing its contents. From 2559913342feb3de83ce668a15bfc965a1c28022 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 08:03:16 +1100 Subject: [PATCH 54/94] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ca1dacba4..c63e8b783 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- Added PyEncoder and support BLP saving #6069 + [radarhere] + +- Handle TGA images with packets that cross scan lines #6087 + [radarhere] + - Added FITS reading #6056 [radarhere, hugovk] From 2f2b48dc2c6f8907968c9deea4fb3a4be57c3c09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 08:32:21 +1100 Subject: [PATCH 55/94] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c63e8b783..421cd7d02 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- Replace requirements.txt with extras #6072 + [hugovk, radarhere] + - Added PyEncoder and support BLP saving #6069 [radarhere] From 13cedb92218eb66f966606c66e35aceabaa8aaf6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 08:36:18 +1100 Subject: [PATCH 56/94] Added release notes for #6069 --- docs/releasenotes/9.1.0.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index aa4b6af06..0db1c2600 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -153,6 +153,12 @@ By default, :py:meth:`~PIL.Image.Image.getpalette` returns RGB data from the pal A ``rawmode`` argument has been added, to allow the mode to be chosen instead. ``None`` can be used to return data in the current mode of the palette. +Added PyEncoder +^^^^^^^^^^^^^^^ + +:py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be +written in Python. + Other Changes ============= @@ -169,3 +175,10 @@ Image._repr_pretty_ ``im._repr_pretty_`` has been added to provide a representation of an image without the identity of the object. This allows Jupyter to describe an image and have that description stay the same on subsequent executions of the same code. + +Added BLP saving +^^^^^^^^^^^^^^^^ + +Support has been added for saving BLP images. ``blp_version`` can be used to specify +whether the image should be saved as BLP1 or BLP2, e.g. +``im.save("out.blp", blp_version="BLP1")``. By default, BLP2 will be used. From f2987d65c00ee403120ba63e23a211bcbe17f4c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 08:27:39 +1100 Subject: [PATCH 57/94] Linked to file codec documentation --- docs/releasenotes/9.1.0.rst | 3 ++- src/PIL/ImageFile.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index 0db1c2600..22b185e95 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -157,7 +157,8 @@ Added PyEncoder ^^^^^^^^^^^^^^^ :py:class:`~PIL.ImageFile.PyEncoder` has been added, allowing for file encoders to be -written in Python. +written in Python. See :ref:`Writing Your Own File Codec in Python` for +more information. Other Changes ============= diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c63cc6145..767b38ca4 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -702,6 +702,8 @@ class PyEncoder(PyCodec): """ Python implementation of a format encoder. Override this class and add the decoding logic in the :meth:`encode` method. + + See :ref:`Writing Your Own File Codec in Python` """ _pushes_fd = False From a606fd85a3cf5b72c42e876364e74e6157202b98 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 14:12:56 +1100 Subject: [PATCH 58/94] Run encoder cleanup method after errors as well --- Tests/test_imagefile.py | 5 ++++ src/PIL/ImageFile.py | 56 ++++++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index f3da73e38..1c444fe27 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -200,6 +200,9 @@ class MockPyEncoder(ImageFile.PyEncoder): def encode(self, buffer): return 1, 1, b"" + def cleanup(self): + self.cleanup_called = True + xoff, yoff, xsize, ysize = 10, 20, 100, 100 @@ -327,10 +330,12 @@ class TestPyEncoder(CodecsTest): im = MockImageFile(buf) fp = BytesIO() + self.encoder.cleanup_called = False with pytest.raises(ValueError): ImageFile._save( im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] ) + assert self.encoder.cleanup_called with pytest.raises(ValueError): ImageFile._save( diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index c63cc6145..e2a9b66cf 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -503,36 +503,40 @@ def _save(im, fp, tile, bufsize=0): # compress to Python file-compatible object for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: - while True: - l, s, d = e.encode(bufsize) - fp.write(d) - if s: - break - if s < 0: - raise OSError(f"encoder error {s} when writing image file") from exc - e.cleanup() + try: + if o > 0: + fp.seek(o) + e.setimage(im.im, b) + if e.pushes_fd: + e.setfd(fp) + l, s = e.encode_to_pyfd() + else: + while True: + l, s, d = e.encode(bufsize) + fp.write(d) + if s: + break + if s < 0: + raise OSError(f"encoder error {s} when writing image file") from exc + finally: + e.cleanup() else: # slight speedup: compress to real file object for e, b, o, a in tile: e = Image._getencoder(im.mode, e, a, im.encoderconfig) - if o > 0: - fp.seek(o) - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: - s = e.encode_to_file(fh, bufsize) - if s < 0: - raise OSError(f"encoder error {s} when writing image file") - e.cleanup() + try: + if o > 0: + fp.seek(o) + e.setimage(im.im, b) + if e.pushes_fd: + e.setfd(fp) + l, s = e.encode_to_pyfd() + else: + s = e.encode_to_file(fh, bufsize) + if s < 0: + raise OSError(f"encoder error {s} when writing image file") + finally: + e.cleanup() if hasattr(fp, "flush"): fp.flush() From 4d868abd8a702c5c873b9d2090b7dd2f3361f834 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 13:15:12 +1100 Subject: [PATCH 59/94] Moved non-codec code outside of try block --- src/PIL/ImageFile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e2a9b66cf..964943ecd 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -223,11 +223,11 @@ class ImageFile(Image.Image): ) ] for decoder_name, extents, offset, args in self.tile: + seek(offset) decoder = Image._getdecoder( self.mode, decoder_name, args, self.decoderconfig ) try: - seek(offset) decoder.setimage(self.im, extents) if decoder.pulls_fd: decoder.setfd(self.fp) @@ -502,10 +502,10 @@ def _save(im, fp, tile, bufsize=0): except (AttributeError, io.UnsupportedOperation) as exc: # compress to Python file-compatible object for e, b, o, a in tile: + if o > 0: + fp.seek(o) e = Image._getencoder(im.mode, e, a, im.encoderconfig) try: - if o > 0: - fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) @@ -523,10 +523,10 @@ def _save(im, fp, tile, bufsize=0): else: # slight speedup: compress to real file object for e, b, o, a in tile: + if o > 0: + fp.seek(o) e = Image._getencoder(im.mode, e, a, im.encoderconfig) try: - if o > 0: - fp.seek(o) e.setimage(im.im, b) if e.pushes_fd: e.setfd(fp) From bb9338e34d705501b87876ebe6a8212b88b96acb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 13:59:52 +1100 Subject: [PATCH 60/94] Removed duplicate code --- src/PIL/ImageFile.py | 55 ++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 964943ecd..18601fb37 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -499,44 +499,33 @@ def _save(im, fp, tile, bufsize=0): try: fh = fp.fileno() fp.flush() - except (AttributeError, io.UnsupportedOperation) as exc: - # compress to Python file-compatible object - for e, b, o, a in tile: - if o > 0: - fp.seek(o) - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - try: - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() - else: + exc = None + except (AttributeError, io.UnsupportedOperation) as e: + exc = e + for e, b, o, a in tile: + if o > 0: + fp.seek(o) + encoder = Image._getencoder(im.mode, e, a, im.encoderconfig) + try: + encoder.setimage(im.im, b) + if encoder.pushes_fd: + encoder.setfd(fp) + l, s = encoder.encode_to_pyfd() + else: + if exc: + # compress to Python file-compatible object while True: - l, s, d = e.encode(bufsize) + l, s, d = encoder.encode(bufsize) fp.write(d) if s: break - if s < 0: - raise OSError(f"encoder error {s} when writing image file") from exc - finally: - e.cleanup() - else: - # slight speedup: compress to real file object - for e, b, o, a in tile: - if o > 0: - fp.seek(o) - e = Image._getencoder(im.mode, e, a, im.encoderconfig) - try: - e.setimage(im.im, b) - if e.pushes_fd: - e.setfd(fp) - l, s = e.encode_to_pyfd() else: - s = e.encode_to_file(fh, bufsize) - if s < 0: - raise OSError(f"encoder error {s} when writing image file") - finally: - e.cleanup() + # slight speedup: compress to real file object + s = encoder.encode_to_file(fh, bufsize) + if s < 0: + raise OSError(f"encoder error {s} when writing image file") from exc + finally: + encoder.cleanup() if hasattr(fp, "flush"): fp.flush() From 949e4afaa962b321ad231cb46da5e19011c5f27b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 09:19:04 +1100 Subject: [PATCH 61/94] Updated Python for doccheck to 3.10 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 414c7e94e..133972881 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: path: Tests/errors - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.9 + if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.10 run: | python3 -m pip install sphinx-copybutton sphinx-issues sphinx-removed-in sphinx-rtd-theme sphinxext-opengraph make doccheck From 3cb355c1afa395b4e90b8407f15561ff52f60bc9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 27 Feb 2022 15:39:25 +1100 Subject: [PATCH 62/94] Refer to set_as_raw() in Python decoder documentation --- docs/handbook/writing-your-own-image-plugin.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 0c9cfe8e8..fc33c8e01 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -432,4 +432,7 @@ Python-based file codec: a buffer of data to be interpreted, or the ``encode`` method is repeatedly called with the size of data to be output. + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be + used to populate the image. + 3. Cleanup: The instance's ``cleanup`` method is called. From 3b79a776d6a6ec009d7e3d23259f62d1b86ff786 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 12:17:16 +1100 Subject: [PATCH 63/94] Removed "Experimental" label from pulls_fd --- .../writing-your-own-image-plugin.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index fc33c8e01..d62be7685 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -373,12 +373,10 @@ interest in this object are: any format specific state or options. **pulls_fd** - **EXPERIMENTAL** -- **WARNING**, interface may change. If set to 1, - ``state->fd`` will be a pointer to the Python file like object. The - decoder may use the functions in ``codec_fd.c`` to read directly - from the file like object rather than have the data pushed through a - buffer. Note that this implementation may be refactored until this - warning is removed. + If set to 1, ``state->fd`` will be a pointer to the Python file like + object. The decoder may use the functions in ``codec_fd.c`` to read + directly from the file like object rather than have the data pushed + through a buffer. .. versionadded:: 3.3.0 @@ -389,17 +387,16 @@ Decoding The decode function is called with the target (core) image, the decoder state structure, and a buffer of data to be decoded. -**Experimental** -- If ``pulls_fd`` is set, then the decode function -is called once, with an empty buffer. It is the decoder's -responsibility to decode the entire tile in that one call. The rest of -this section only applies if ``pulls_fd`` is not set. - It is the decoder's responsibility to pull as much data as possible out of the buffer and return the number of bytes consumed. The next call to the decoder will include the previous unconsumed tail. The decoder function will be called multiple times as the data is read from the file like object. +Alternatively, if ``pulls_fd`` is set, then the decode function is +called once, with an empty buffer. It is the decoder's responsibility +to decode the entire tile in that one call. + If an error occurs, set ``state->errcode`` and return -1. Return -1 on success, without setting the errcode. From 95e31944971d4441430e82047e93aacd905f72c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Feb 2022 12:21:29 +1100 Subject: [PATCH 64/94] Improved documentation for _pulls_fd and _pushes_fd --- docs/handbook/writing-your-own-image-plugin.rst | 11 ++++++++++- src/PIL/ImageFile.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index d62be7685..3efea1426 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -395,7 +395,9 @@ from the file like object. Alternatively, if ``pulls_fd`` is set, then the decode function is called once, with an empty buffer. It is the decoder's responsibility -to decode the entire tile in that one call. +to decode the entire tile in that one call. Using this will provide a +codec with more freedom, but that freedom may mean increased memory usage +if entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. @@ -429,6 +431,13 @@ Python-based file codec: a buffer of data to be interpreted, or the ``encode`` method is repeatedly called with the size of data to be output. + Alternatively, if the decoder's ``_pulls_fd`` property (or the encoder's + ``_pushes_fd`` property) is set to ``True``, then ``decode`` and ``encode`` + will only be called once. In the decoder, ``self.fd`` can be used to access + the file-like object. Using this will provide a codec with more freedom, but + that freedom may mean increased memory usage if entire file is held in + memory at once by the codec. + In ``decode``, once the data has been interpreted, ``set_as_raw`` can be used to populate the image. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 767b38ca4..acfb1a734 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -725,6 +725,9 @@ class PyEncoder(PyCodec): def encode_to_pyfd(self): """ + If ``pushes_fd`` is ``True``, then this method will be used, + and ``encode()`` will only be called once. + :returns: A tuple of ``(bytes consumed, errcode)``. Err codes are from :data:`.ImageFile.ERRORS`. """ From d05281fe691b4f0b543c316a42cf0d129a270151 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 13:38:20 +1100 Subject: [PATCH 65/94] Improved documentation for cleanup() in Python codecs --- docs/handbook/writing-your-own-image-plugin.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 3efea1426..8a3dcf69f 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -441,4 +441,9 @@ Python-based file codec: In ``decode``, once the data has been interpreted, ``set_as_raw`` can be used to populate the image. -3. Cleanup: The instance's ``cleanup`` method is called. +3. Cleanup: The instance's ``cleanup`` method is called once the transformation + is complete. This can be used to clean up any resources used by the codec. + + If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you + probably chose to perform any cleanup tasks at the end of ``decode`` or + ``encode``. From 4615e1d42a5d0a1d09b5724ddf0fc2f593e7fac9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 14:13:40 +1100 Subject: [PATCH 66/94] Document alternatives to accessing individual pixels --- docs/reference/PixelAccess.rst | 10 ++++++++-- docs/reference/PyAccess.rst | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index 173a0bcc0..d2e80fb8c 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -6,7 +6,13 @@ The PixelAccess class provides read and write access to :py:class:`PIL.Image` data at a pixel level. -.. note:: Accessing individual pixels is fairly slow. If you are looping over all of the pixels in an image, there is likely a faster way using other parts of the Pillow API. +.. note:: Accessing individual pixels is fairly slow. If you are + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- @@ -39,7 +45,7 @@ Access using negative indexes is also possible. :py:class:`PixelAccess` Class ------------------------------------ +----------------------------- .. class:: PixelAccess diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index e77944d20..f9eb9b524 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -7,8 +7,12 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the :ref:`PixelAccess`. This implementation is far faster on PyPy than the PixelAccess version. .. note:: Accessing individual pixels is fairly slow. If you are - looping over all of the pixels in an image, there is likely - a faster way using other parts of the Pillow API. + looping over all of the pixels in an image, there is likely + a faster way using other parts of the Pillow API. + + :mod:`~PIL.Image`, :mod:`~PIL.ImageChops` and :mod:`~PIL.ImageOps` + have methods for many standard operations. If you wish to perform + a custom mapping, check out :py:meth:`~PIL.Image.Image.point`. Example ------- From 0cd550719a5903553ba3bcd6879cc0bb2ab49f94 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 14:56:04 +1100 Subject: [PATCH 67/94] Link to demonstrations of PyDecoder and PyEncoder --- docs/handbook/writing-your-own-image-plugin.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 8a3dcf69f..28e257be5 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -447,3 +447,9 @@ Python-based file codec: If you set ``_pulls_fd`` or ``_pushes_fd`` to ``True`` however, then you probably chose to perform any cleanup tasks at the end of ``decode`` or ``encode``. + +For an example :py:class:`PIL.ImageFile.PyDecoder`, see `DdsImagePlugin +`_. +For a plugin that uses both :py:class:`PIL.ImageFile.PyDecoder` and +:py:class:`PIL.ImageFile.PyEncoder`, see `BlpImagePlugin +`_ From a7e8a386d37d0c01106b7fa1343c957b51281887 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 15:44:12 +1100 Subject: [PATCH 68/94] Mention PsdImagePlugin as a plugin using multiple tiles --- docs/handbook/writing-your-own-image-plugin.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 28e257be5..423e9c55c 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -123,8 +123,12 @@ The ``tile`` attribute To be able to read the file as well as just identifying it, the ``tile`` attribute must also be set. This attribute consists of a list of tile descriptors, where each descriptor specifies how data should be loaded to a -given region in the image. In most cases, only a single descriptor is used, -covering the full image. +given region in the image. + +In most cases, only a single descriptor is used, covering the full image. +:py:class:`.PsdImagePlugin.PsdImageFile` uses multiple tiles to combine +channels within a single layer, given that the channels are stored separately, +one after the other. The tile descriptor is a 4-tuple with the following contents:: From 2b9cc3ccc534f3d02786d52e2fa09af785d0fd32 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 16:34:21 +1100 Subject: [PATCH 69/94] Documented writing your own encoder in C --- .../writing-your-own-image-plugin.rst | 94 +++++++++---------- src/PIL/Image.py | 4 +- 2 files changed, 48 insertions(+), 50 deletions(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 423e9c55c..59f26d3b2 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -328,42 +328,42 @@ The fields are used as follows: Whether the first line in the image is the top line on the screen (1), or the bottom line (-1). If omitted, the orientation defaults to 1. -.. _file-decoders: +.. _file-codecs: -Writing Your Own File Decoder in C -================================== +Writing Your Own File Codec in C +================================ -There are 3 stages in a file decoder's lifetime: +There are 3 stages in a file codec's lifetime: -1. Setup: Pillow looks for a function in the decoder registry, falling - back to a function named ``[decodername]_decoder`` on the internal - core image object. That function is called with the ``args`` tuple - from the ``tile`` setup in the ``_open`` method. +1. Setup: Pillow looks for a function in the decoder or encoder registry, + falling back to a function named ``[codecname]_decoder`` or + ``[codecname]_encoder`` on the internal core image object. That function is + called with the ``args`` tuple from the ``tile``. -2. Decoding: The decoder's decode function is repeatedly called with - chunks of image data. +2. Transforming: The codec's ``decode`` or ``encode`` function is repeatedly + called with chunks of image data. -3. Cleanup: If the decoder has registered a cleanup function, it will - be called at the end of the decoding process, even if there was an +3. Cleanup: If the codec has registered a cleanup function, it will + be called at the end of the transformation process, even if there was an exception raised. Setup ----- -The current conventions are that the decoder setup function is named -``PyImaging_[Decodername]DecoderNew`` and defined in ``decode.c``. The -python binding for it is named ``[decodername]_decoder`` and is setup -from within the ``_imaging.c`` file in the codecs section of the -function array. +The current conventions are that the codec setup function is named +``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` +and defined in ``decode.c`` or ``encode.c``. The Python binding for it is +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is setup from +within the ``_imaging.c`` file in the codecs section of the function array. -The setup function needs to call ``PyImaging_DecoderNew`` and at the -very least, set the ``decode`` function pointer. The fields of -interest in this object are: +The setup function needs to call ``PyImaging_DecoderNew`` or +``PyImaging_EncoderNew`` and at the very least, set the ``decode`` or +``encode`` function pointer. The fields of interest in this object are: -**decode** - Function pointer to the decode function, which has access to - ``im``, ``state``, and the buffer of data to be added to the image. +**decode**/**encode** + Function pointer to the decode or encode function, which has access to + ``im``, ``state``, and the buffer of data to be transformed. **cleanup** Function pointer to the cleanup function, has access to ``state``. @@ -373,35 +373,34 @@ interest in this object are: **state** An ImagingCodecStateInstance, will be set by Pillow. The ``context`` - member is an opaque struct that can be used by the decoder to store + member is an opaque struct that can be used by the codec to store any format specific state or options. -**pulls_fd** - If set to 1, ``state->fd`` will be a pointer to the Python file like - object. The decoder may use the functions in ``codec_fd.c`` to read - directly from the file like object rather than have the data pushed - through a buffer. +**pulls_fd**/**pushes_fd** + If the decoder has ``pulls_fd`` or the encoder has ``pushes_fd`` set to 1, + ``state->fd`` will be a pointer to the Python file like object. The codec may + use the functions in ``codec_fd.c`` to read or write directly with the file + like object rather than have the data pushed through a buffer. .. versionadded:: 3.3.0 -Decoding --------- +Transforming +------------ -The decode function is called with the target (core) image, the -decoder state structure, and a buffer of data to be decoded. +The decode or encode function is called with the target (core) image, the codec +state structure, and a buffer of data to be transformed. -It is the decoder's responsibility to pull as much data as possible -out of the buffer and return the number of bytes consumed. The next -call to the decoder will include the previous unconsumed tail. The -decoder function will be called multiple times as the data is read -from the file like object. +It is the codec's responsibility to pull as much data as possible out of the +buffer and return the number of bytes consumed. The next call to the codec will +include the previous unconsumed tail. The codec function will be called +multiple times as the data processed. -Alternatively, if ``pulls_fd`` is set, then the decode function is -called once, with an empty buffer. It is the decoder's responsibility -to decode the entire tile in that one call. Using this will provide a -codec with more freedom, but that freedom may mean increased memory usage -if entire tile is held in memory at once by the codec. +Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or +encode function is called once, with an empty buffer. It is the codec's +responsibility to transform the entire tile in that one call. Using this will +provide a codec with more freedom, but that freedom may mean increased memory +usage if entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. @@ -410,10 +409,9 @@ Return -1 on success, without setting the errcode. Cleanup ------- -The cleanup function is called after the decoder returns a negative -value, or if there is a read error from the file. This function should -free any allocated memory and release any resources from external -libraries. +The cleanup function is called after the codec returns a negative +value, or if there is an error. This function should free any allocated +memory and release any resources from external libraries. .. _file-codecs-py: @@ -428,7 +426,7 @@ They should be registered using :py:meth:`PIL.Image.register_decoder` and the file codecs, there are three stages in the lifetime of a Python-based file codec: -1. Setup: Pillow looks for the decoder in the registry, then +1. Setup: Pillow looks for the codec in the decoder or encoder registry, then instantiates the class. 2. Transforming: The instance's ``decode`` method is repeatedly called with diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c9265b5ab..a8cd214dd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2779,9 +2779,9 @@ def frombytes(mode, size, data, decoder_name="raw", *args): In its simplest form, this function takes three arguments (mode, size, and unpacked pixel data). - You can also use any pixel decoder supported by PIL. For more + You can also use any pixel decoder supported by PIL. For more information on available decoders, see the section - :ref:`Writing Your Own File Decoder `. + :ref:`Writing Your Own File Codec `. Note that this function decodes pixel data only, not entire images. If you have an entire image in a string, wrap it in a From 573cf278f1ab50c670447a9d39d582ae9d13bd31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 19:10:10 +1100 Subject: [PATCH 70/94] Reset size when seeking away from "Large Thumbnail" MPO frame --- Tests/test_file_mpo.py | 3 +++ src/PIL/MpoImagePlugin.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 0e59e4d56..0fa3b6382 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -85,6 +85,9 @@ def test_frame_size(): im.seek(1) assert im.size == (680, 480) + im.seek(0) + assert im.size == (640, 480) + def test_ignore_frame_size(): # Ignore the different size of the second frame diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 7ccf27c42..88c1bfcc5 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -46,6 +46,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self._after_jpeg_open() def _after_jpeg_open(self, mpheader=None): + self._initial_size = self.size self.mpinfo = mpheader if mpheader is not None else self._getmp() self.n_frames = self.mpinfo[0xB001] self.__mpoffsets = [ @@ -77,6 +78,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): segment = self.fp.read(2) if not segment: raise ValueError("No data found for frame") + self._size = self._initial_size if i16(segment) == 0xFFE1: # APP1 n = i16(self.fp.read(2)) - 2 self.info["exif"] = ImageFile._safe_read(self.fp, n) From 5c6212052cc735b5aabc895bb18264143b8408c7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 20:11:35 +1100 Subject: [PATCH 71/94] Convert subsequent frames of L mode GIF to LA if transparency is present --- Tests/images/no_palette_with_transparency.gif | Bin 0 -> 64 bytes Tests/test_file_gif.py | 11 ++++++++++ src/PIL/GifImagePlugin.py | 19 +++++++++++++----- 3 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 Tests/images/no_palette_with_transparency.gif diff --git a/Tests/images/no_palette_with_transparency.gif b/Tests/images/no_palette_with_transparency.gif new file mode 100644 index 0000000000000000000000000000000000000000..3cd1c0c48eb5bcd8c572b1ad034955190d801e6a GIT binary patch literal 64 ncmZ?wbhEHbWMp7u00PCIEI|4{gARxT7UN)HU}RyzEny7+38e<8 literal 0 HcmV?d00001 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 011d982f0..924adad9e 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -59,6 +59,17 @@ def test_invalid_file(): GifImagePlugin.GifImageFile(invalid_file) +def test_l_mode_transparency(): + with Image.open("Tests/images/no_palette_with_transparency.gif") as im: + assert im.mode == "L" + assert im.load()[0, 0] == 0 + assert im.info["transparency"] == 255 + + im.seek(1) + assert im.mode == "LA" + assert im.load()[0, 0] == (0, 255) + + def test_optimize(): def test_grayscale(optimize): im = Image.new("L", (1, 1), 0) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4f8ea209d..b222235d8 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -167,9 +167,15 @@ class GifImageFile(ImageFile.ImageFile): if self.__frame == 1: self.pyaccess = None if "transparency" in self.info: - self.mode = "RGBA" - self.im.putpalettealpha(self.info["transparency"], 0) - self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) + if self.mode == "P": + self.im.putpalettealpha(self.info["transparency"], 0) + self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) + self.mode = "RGBA" + else: + self.im = self.im.convert_transparent( + "LA", self.info["transparency"] + ) + self.mode = "LA" del self.info["transparency"] else: @@ -368,8 +374,11 @@ class GifImageFile(ImageFile.ImageFile): if self.__frame == 0: return if self._frame_transparency is not None: - self.im.putpalettealpha(self._frame_transparency, 0) - frame_im = self.im.convert("RGBA") + if self.mode == "P": + self.im.putpalettealpha(self._frame_transparency, 0) + frame_im = self.im.convert("RGBA") + else: + frame_im = self.im.convert_transparent("LA", self._frame_transparency) else: frame_im = self.im.convert("RGB") frame_im = self._crop(frame_im, self.dispose_extent) From e2b007f0c2117beee854d25eb801a78243dd1282 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 20:25:25 +1100 Subject: [PATCH 72/94] Use transparency info key when converting to LA --- Tests/test_image_convert.py | 4 ++++ src/PIL/Image.py | 4 +++- src/libImaging/Convert.c | 39 ++++++++++++++++--------------------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1d6469819..1ffd012aa 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -135,6 +135,10 @@ def test_trns_l(tmp_path): f = str(tmp_path / "temp.png") + im_la = im.convert("LA") + assert "transparency" not in im_la.info + im_la.save(f) + im_rgb = im.convert("RGB") assert im_rgb.info["transparency"] == (128, 128, 128) # undone im_rgb.save(f) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c9265b5ab..352d77fe7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -975,7 +975,9 @@ class Image: delete_trns = False # transparency handling if has_transparency: - if self.mode in ("1", "L", "I", "RGB") and mode == "RGBA": + if (self.mode in ("1", "L", "I") and mode in ("LA", "RGBA")) or ( + self.mode == "RGB" and mode == "RGBA" + ): # Use transparent conversion to promote from transparent # color to an alpha channel. new_im = self._new( diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 0f200af6b..5a632ca42 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -1634,29 +1634,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { return (Imaging)ImagingError_ModeError(); } - if (!((strcmp(imIn->mode, "RGB") == 0 || strcmp(imIn->mode, "1") == 0 || - strcmp(imIn->mode, "I") == 0 || strcmp(imIn->mode, "L") == 0) && - strcmp(mode, "RGBA") == 0)) -#ifdef notdef - { - return (Imaging)ImagingError_ValueError("conversion not supported"); - } -#else - { - static char buf[100]; - snprintf( - buf, - 100, - "conversion from %.10s to %.10s not supported in convert_transparent", - imIn->mode, - mode); - return (Imaging)ImagingError_ValueError(buf); - } -#endif - - if (strcmp(imIn->mode, "RGB") == 0) { + if (strcmp(imIn->mode, "RGB") == 0 && strcmp(mode, "RGBA") == 0) { convert = rgb2rgba; - } else { + } else if ((strcmp(imIn->mode, "1") == 0 || + strcmp(imIn->mode, "I") == 0 || + strcmp(imIn->mode, "L") == 0 + ) && ( + strcmp(mode, "RGBA") == 0 || + strcmp(mode, "LA") == 0 + )) { if (strcmp(imIn->mode, "1") == 0) { convert = bit2rgb; } else if (strcmp(imIn->mode, "I") == 0) { @@ -1665,6 +1651,15 @@ ImagingConvertTransparent(Imaging imIn, const char *mode, int r, int g, int b) { convert = l2rgb; } g = b = r; + } else { + static char buf[100]; + snprintf( + buf, + 100, + "conversion from %.10s to %.10s not supported in convert_transparent", + imIn->mode, + mode); + return (Imaging)ImagingError_ValueError(buf); } imOut = ImagingNew2Dirty(mode, imOut, imIn); From 7d3274518d2e18559f164cd2335c7700249763d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Mar 2022 21:05:42 +1100 Subject: [PATCH 73/94] Allow LA to be used as a mask in paste() --- Tests/test_image_paste.py | 32 ++++++++++++++++++++++++++++++++ src/PIL/GifImagePlugin.py | 2 +- src/PIL/Image.py | 6 +++--- src/libImaging/Paste.c | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index dc3caef01..281d5a6fb 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -67,6 +67,16 @@ class TestImagingPaste: ], ) + @cached_property + def gradient_LA(self): + return Image.merge( + "LA", + [ + self.gradient_L, + self.gradient_L.transpose(Image.Transpose.ROTATE_90), + ], + ) + @cached_property def gradient_RGBA(self): return Image.merge( @@ -145,6 +155,28 @@ class TestImagingPaste: ], ) + def test_image_mask_LA(self): + for mode in ("RGBA", "RGB", "L"): + im = Image.new(mode, (200, 200), "white") + im2 = getattr(self, "gradient_" + mode) + + self.assert_9points_paste( + im, + im2, + self.gradient_LA, + [ + (128, 191, 255, 191), + (112, 207, 206, 111), + (128, 254, 128, 1), + (208, 208, 239, 239), + (192, 191, 191, 191), + (207, 207, 112, 113), + (255, 255, 255, 255), + (239, 207, 207, 239), + (255, 191, 128, 191), + ], + ) + def test_image_mask_RGBA(self): for mode in ("RGBA", "RGB", "L"): im = Image.new(mode, (200, 200), "white") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b222235d8..12d85263b 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -385,7 +385,7 @@ class GifImageFile(ImageFile.ImageFile): self.im = self._prev_im self.mode = self.im.mode - if frame_im.mode == "RGBA": + if frame_im.mode in ("LA", "RGBA"): self.im.paste(frame_im, self.dispose_extent, frame_im) else: self.im.paste(frame_im, self.dispose_extent) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 352d77fe7..01bdb3612 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1566,8 +1566,8 @@ class Image: also use color strings as supported by the ImageColor module. If a mask is given, this method updates only the regions - indicated by the mask. You can use either "1", "L" or "RGBA" - images (in the latter case, the alpha band is used as mask). + indicated by the mask. You can use either "1", "L", "LA", "RGBA" + or "RGBa" images (if present, the alpha band is used as mask). Where the mask is 255, the given image is copied as is. Where the mask is 0, the current value is preserved. Intermediate values will mix the two images together, including their alpha @@ -1615,7 +1615,7 @@ class Image: elif isImageType(im): im.load() if self.mode != im.mode: - if self.mode != "RGB" or im.mode not in ("RGBA", "RGBa"): + if self.mode != "RGB" or im.mode not in ("LA", "RGBA", "RGBa"): # should use an adapter for this! im = im.convert(self.mode) im = im.im diff --git a/src/libImaging/Paste.c b/src/libImaging/Paste.c index be26cd260..fafd8141e 100644 --- a/src/libImaging/Paste.c +++ b/src/libImaging/Paste.c @@ -295,7 +295,7 @@ ImagingPaste( paste_mask_L(imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); ImagingSectionLeave(&cookie); - } else if (strcmp(imMask->mode, "RGBA") == 0) { + } else if (strcmp(imMask->mode, "LA") == 0 || strcmp(imMask->mode, "RGBA") == 0) { ImagingSectionEnter(&cookie); paste_mask_RGBA( imOut, imIn, imMask, dx0, dy0, sx0, sy0, xsize, ysize, pixelsize); From 211639b4f35a1b82def3278990c6f6b2c8f8b5c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Mar 2022 11:00:28 +1100 Subject: [PATCH 74/94] Updated harfbuzz to 4.0.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 7aa46274b..3ff1aeca0 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/3.4.0.zip", - "filename": "harfbuzz-3.4.0.zip", - "dir": "harfbuzz-3.4.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/4.0.0.zip", + "filename": "harfbuzz-4.0.0.zip", + "dir": "harfbuzz-4.0.0", "build": [ cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), cmd_nmake(target="clean"), From 81a4c410223122ba49d442dc81d5ec1c14ad211a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Mar 2022 17:36:26 +1100 Subject: [PATCH 75/94] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 421cd7d02..08d11e986 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- Reset size when seeking away from "Large Thumbnail" MPO frame #6101 + [radarhere] + - Replace requirements.txt with extras #6072 [hugovk, radarhere] From de968dd920eaa3d1a27877059c6bbb9043a9d26b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 2 Mar 2022 21:21:25 +1100 Subject: [PATCH 76/94] Document that histogram() uses 256 bins per channel --- docs/reference/ImageStat.rst | 10 ++++++++++ src/PIL/Image.py | 11 ++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/reference/ImageStat.rst b/docs/reference/ImageStat.rst index 5bb735296..f61d12313 100644 --- a/docs/reference/ImageStat.rst +++ b/docs/reference/ImageStat.rst @@ -14,6 +14,16 @@ for a region of an image. statistics. You can also pass in a previously calculated histogram. :param image: A PIL image, or a precalculated histogram. + + .. note:: + + For a PIL image, calculations rely on the + :py:meth:`~PIL.Image.Image.histogram` method. The pixel counts are + grouped into 256 bins, even if the image has more than 8 bits per + channel. So ``I`` and ``F`` mode images have a maximum ``mean``, + ``median`` and ``rms`` of 255, and cannot have an ``extrema`` maximum + of more than 255. + :param mask: An optional mask. .. py:attribute:: extrema diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c9265b5ab..4b8d2c750 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1492,11 +1492,12 @@ class Image: def histogram(self, mask=None, extrema=None): """ - Returns a histogram for the image. The histogram is returned as - a list of pixel counts, one for each pixel value in the source - image. If the image has more than one band, the histograms for - all bands are concatenated (for example, the histogram for an - "RGB" image contains 768 values). + Returns a histogram for the image. The histogram is returned as a + list of pixel counts, one for each pixel value in the source + image. Counts are grouped into 256 bins for each band, even if + the image has more than 8 bits per band. If the image has more + than one band, the histograms for all bands are concatenated (for + example, the histogram for an "RGB" image contains 768 values). A bilevel image (mode "1") is treated as a greyscale ("L") image by this method. From c27519960f4d9bbcb089cc6c2903ce518a500276 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Mar 2022 22:10:19 +1100 Subject: [PATCH 77/94] Check if self.im is not None --- Tests/test_image_access.py | 15 +++++++++------ src/PIL/GifImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 2 +- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/Image.py | 4 ++-- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 7b3036979..bdbfdd0e2 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -154,14 +154,17 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), None) - with pytest.raises(IndexError): + assert im.load() is not None + + error = ValueError if self._need_cffi_access else IndexError + with pytest.raises(error): im.putpixel((0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.putpixel((-1, -1), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) # check initial color @@ -176,10 +179,10 @@ class TestImageGetPixel(AccessTest): # Check 0 im = Image.new(mode, (0, 0), c) - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index - with pytest.raises(IndexError): + with pytest.raises(error): im.getpixel((-1, -1)) def test_basic(self): diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 4f8ea209d..f3608cdef 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -298,7 +298,7 @@ class GifImageFile(ImageFile.ImageFile): self.dispose = Image.core.fill(dispose_mode, dispose_size, color) else: # replace with previous contents - if self.im: + if self.im is not None: # only dispose the extent in this frame self.dispose = self._crop(self.im, self.dispose_extent) elif frame_transparency is not None: diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 069aff96b..57e3ea12d 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -287,7 +287,7 @@ class IcnsImageFile(ImageFile.ImageFile): ) px = Image.Image.load(self) - if self.im and self.im.size == self.size: + if self.im is not None and self.im.size == self.size: # Already loaded return px self.load_prepare() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index b4f84ee20..915e4c928 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -304,7 +304,7 @@ class IcoImageFile(ImageFile.ImageFile): self._size = value def load(self): - if self.im and self.im.size == self.size: + if self.im is not None and self.im.size == self.size: # Already loaded return Image.Image.load(self) im = self.ico.getimage(self.size) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c9265b5ab..5efecf0bf 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -847,7 +847,7 @@ class Image: :returns: An image access object. :rtype: :ref:`PixelAccess` or :py:class:`PIL.PyAccess` """ - if self.im and self.palette and self.palette.dirty: + if self.im is not None and self.palette and self.palette.dirty: # realize palette mode, arr = self.palette.getdata() self.im.putpalette(mode, arr) @@ -864,7 +864,7 @@ class Image: self.palette.mode = palette_mode self.palette.palette = self.im.getpalette(palette_mode, palette_mode) - if self.im: + if self.im is not None: if cffi and USE_CFFI_ACCESS: if self.pyaccess: return self.pyaccess From e2e87d73c39612781be059072a2a08c30f743459 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 11:01:37 +1100 Subject: [PATCH 78/94] Reverted SyntaxError change to match other plugins --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 725ddec10..9ed34a0cd 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,7 +94,7 @@ class PpmImageFile(ImageFile.ImageFile): try: mode = MODES[magic_number] except KeyError: - raise SyntaxError("Not a PPM image file") from None + raise SyntaxError("not a PPM file") self.custom_mimetype = { b"P4": "image/x-portable-bitmap", From cb4e26783f4c183ed918adc375c5535484b8aac7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 10:36:34 +1100 Subject: [PATCH 79/94] Retain variable case for backwards compatibility --- src/PIL/PpmImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9ed34a0cd..4c9e7292d 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -20,7 +20,7 @@ from . import Image, ImageFile # # -------------------------------------------------------------------- -B_WHITESPACE = b"\x20\x09\x0a\x0b\x0c\x0d" +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard @@ -52,7 +52,7 @@ class PpmImageFile(ImageFile.ImageFile): def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) - if c in B_WHITESPACE: + if c in b_whitespace: break magic += c if len(magic) > 6: # exceeded max magic number length @@ -69,7 +69,7 @@ class PpmImageFile(ImageFile.ImageFile): if c == b"#": # found comment, ignore it _ignore_comment() continue - if c in B_WHITESPACE: # found whitespace, ignore it + if c in b_whitespace: # found whitespace, ignore it if c == b"": # reached EOF raise ValueError("Reached EOF while reading header") continue @@ -82,7 +82,7 @@ class PpmImageFile(ImageFile.ImageFile): if c == b"#": _ignore_comment() continue - if c in B_WHITESPACE: # token ended + if c in b_whitespace: # token ended break token += c if len(token) > 10: From 3426052874c765ace64dc3c116114eff0b1ec9c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 12:13:08 +1100 Subject: [PATCH 80/94] Removed re-raising of exception --- src/PIL/PpmImagePlugin.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 4c9e7292d..bb0972bf8 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -109,13 +109,7 @@ class PpmImageFile(ImageFile.ImageFile): self.mode = rawmode = mode for ix in range(3): - token = self._read_token() - try: # check token sanity - token = int(token) - except ValueError: - raise ValueError( - f"Non-decimal-ASCII found in header: {token}" - ) from None + token = int(self._read_token()) if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size From f5b9e2c43af136c13b27c393dba2caaa93d9fd02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 11:17:25 +1100 Subject: [PATCH 81/94] Explicitly check if magic number is empty --- src/PIL/PpmImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index bb0972bf8..89fbc6f34 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -52,7 +52,7 @@ class PpmImageFile(ImageFile.ImageFile): def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) - if c in b_whitespace: + if not c or c in b_whitespace: break magic += c if len(magic) > 6: # exceeded max magic number length @@ -69,9 +69,9 @@ class PpmImageFile(ImageFile.ImageFile): if c == b"#": # found comment, ignore it _ignore_comment() continue + if not c: + raise ValueError("Reached EOF while reading header") if c in b_whitespace: # found whitespace, ignore it - if c == b"": # reached EOF - raise ValueError("Reached EOF while reading header") continue break From d96830115feb9e472c07da5d8982d44a9abbaaa3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 15:22:41 +1100 Subject: [PATCH 82/94] Updated tests --- Tests/test_file_ppm.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fc930e314..0e4f1ba68 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -50,7 +50,7 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, f) -def test_not_ppm(tmp_path): +def test_magic(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"PyInvalid") @@ -69,10 +69,10 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_nondecimal_header(tmp_path): +def test_non_integer_token(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: - f.write(b"P6\n128\x00") + f.write(b"P6\nTEST") with pytest.raises(ValueError): with Image.open(path): @@ -82,32 +82,38 @@ def test_nondecimal_header(tmp_path): def test_token_too_long(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: - f.write(b"P6\n 0123456789") + f.write(b"P6\n 01234567890") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Token too long in file header: b'01234567890'" + def test_too_many_colors(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n1 1\n1000\n") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Too many colors for band: 1000" + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: f.write("P6") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Reached EOF while reading header" + def test_neg_ppm(): # Storage.c accepted negative values for xsize, ysize. the From 4f8173f53f362d92bcb650cf555fbd7c45b6590f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 14:58:05 +1100 Subject: [PATCH 83/94] Refactored to reduce risk of infinite loop --- src/PIL/PpmImagePlugin.py | 50 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 89fbc6f34..8ebc2b063 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,44 +49,38 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self, magic=b""): - while True: # read until next whitespace + def _read_magic(self): + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): c = self.fp.read(1) if not c or c in b_whitespace: break magic += c - if len(magic) > 6: # exceeded max magic number length - break return magic - def _read_token(self, token=b""): - def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF - while self.fp.read(1) not in b"\r\n": - pass - - while True: # read until non-whitespace is found + def _read_token(self): + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) - if c == b"#": # found comment, ignore it - _ignore_comment() - continue if not c: - raise ValueError("Reached EOF while reading header") - if c in b_whitespace: # found whitespace, ignore it - continue - break - - token += c - - while True: # read until next whitespace - c = self.fp.read(1) - if c == b"#": - _ignore_comment() - continue - if c in b_whitespace: # token ended break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue token += c - if len(token) > 10: - raise ValueError(f"Token too long in file header: {token}") + if not token: + # Token was not even 1 byte + raise ValueError("Reached EOF while reading header") + elif len(token) > 10: + raise ValueError(f"Token too long in file header: {token}") return token def _open(self): From f7504b1ef91501105a430a891c335a4ff70418fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 15:49:37 +1100 Subject: [PATCH 84/94] Changed variable --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 8ebc2b063..9d32927d4 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ class PpmImageFile(ImageFile.ImageFile): if maxval > 255: if not mode == "L": raise ValueError(f"Too many colors for band: {token}") - if token < 2 ** 16: + if maxval < 2 ** 16: self.mode = "I" rawmode = "I;16B" else: From d1124cd2b920fa113859309fac138063016b9280 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 16:35:28 +1100 Subject: [PATCH 85/94] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 08d11e986..904a61ce1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- Improved handling of PPM header #5121 + [Piolie, radarhere] + - Reset size when seeking away from "Large Thumbnail" MPO frame #6101 [radarhere] From 72b7ab54bd482035391a9557decfde88c41a1ae4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 16:42:24 +1100 Subject: [PATCH 86/94] pre-commit autoupdate --freeze --- .pre-commit-config.yaml | 4 +- Tests/32bit_segfault_check.py | 2 +- Tests/check_large_memory.py | 2 +- Tests/check_large_memory_numpy.py | 2 +- Tests/test_core_resources.py | 4 +- Tests/test_file_libtiff.py | 4 +- Tests/test_file_tiff_metadata.py | 14 ++--- Tests/test_file_webp.py | 2 +- Tests/test_image_access.py | 10 ++-- Tests/test_image_putdata.py | 2 +- Tests/test_imagecms.py | 2 +- Tests/test_imageqt.py | 6 +- Tests/test_imagestat.py | 4 +- Tests/test_map.py | 2 +- Tests/test_pdfparser.py | 2 +- setup.py | 2 +- src/PIL/BmpImagePlugin.py | 4 +- src/PIL/GimpPaletteFile.py | 2 +- src/PIL/ImImagePlugin.py | 2 +- src/PIL/ImageStat.py | 2 +- src/PIL/ImtImagePlugin.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/PdfParser.py | 98 +++++++++++++++---------------- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 4 +- src/PIL/TiffImagePlugin.py | 10 ++-- src/PIL/XbmImagePlugin.py | 2 +- winbuild/build_prepare.py | 2 +- 28 files changed, 98 insertions(+), 98 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 822fa43ca..b54650565 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: f1d4e742c91dd5179d742b0db9293c4472b765f8 # frozen: 21.12b0 + rev: fc0be6eb1e2a96091e6f64009ee5e9081bf8b6c6 # frozen: 22.1.0 hooks: - id: black args: ["--target-version", "py37"] @@ -19,7 +19,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: 3592548bbd98528887eeed63486cf6c9bae00b98 # frozen: v1.1.10 + rev: ca52c4245639abd55c970e6bbbca95cab3de22d8 # frozen: v1.1.13 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py index e19cdf7a9..2ff7f908f 100755 --- a/Tests/32bit_segfault_check.py +++ b/Tests/32bit_segfault_check.py @@ -4,5 +4,5 @@ import sys from PIL import Image -if sys.maxsize < 2 ** 32: +if sys.maxsize < 2**32: im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index c191ffc1e..d98f4a694 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -23,7 +23,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 70ae6d230..24cb1f722 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -19,7 +19,7 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="requires 64-bit system") +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") def _write_png(tmp_path, xdim, ydim): diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 6c52d25a4..385192a3c 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -110,9 +110,9 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_blocks_max(-1) - if sys.maxsize < 2 ** 32: + if sys.maxsize < 2**32: with pytest.raises(ValueError): - Image.core.set_blocks_max(2 ** 29) + Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 53ed2520a..a9337f4fc 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -218,7 +218,7 @@ class TestFileLibTiff(LibTiffTestCase): values = { 2: "test", 3: 1, - 4: 2 ** 20, + 4: 2**20, 5: TiffImagePlugin.IFDRational(100, 1), 12: 1.05, } @@ -1019,7 +1019,7 @@ class TestFileLibTiff(LibTiffTestCase): im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") - TiffImagePlugin.STRIP_SIZE = 2 ** 18 + TiffImagePlugin.STRIP_SIZE = 2**18 try: im.save(out, compression="tiff_adobe_deflate") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 2213af5aa..056295516 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -258,7 +258,7 @@ def test_ifd_unsigned_rational(tmp_path): im = hopper() info = TiffImagePlugin.ImageFileDirectory_v2() - max_long = 2 ** 32 - 1 + max_long = 2**32 - 1 # 4 bytes unsigned long numerator = max_long @@ -290,8 +290,8 @@ def test_ifd_signed_rational(tmp_path): info = TiffImagePlugin.ImageFileDirectory_v2() # pair of 4 byte signed longs - numerator = 2 ** 31 - 1 - denominator = -(2 ** 31) + numerator = 2**31 - 1 + denominator = -(2**31) info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -302,8 +302,8 @@ def test_ifd_signed_rational(tmp_path): assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator - numerator = -(2 ** 31) - denominator = 2 ** 31 - 1 + numerator = -(2**31) + denominator = 2**31 - 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -315,7 +315,7 @@ def test_ifd_signed_rational(tmp_path): assert denominator == reloaded.tag_v2[37380].denominator # out of bounds of 4 byte signed long - numerator = -(2 ** 31) - 1 + numerator = -(2**31) - 1 denominator = 1 info[37380] = TiffImagePlugin.IFDRational(numerator, denominator) @@ -324,7 +324,7 @@ def test_ifd_signed_rational(tmp_path): im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: - assert 2 ** 31 - 1 == reloaded.tag_v2[37380].numerator + assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 55897f1eb..051119378 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -128,7 +128,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) - @pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 7b3036979..97321143d 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -205,10 +205,10 @@ class TestImageGetPixel(AccessTest): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* for mode in ("I;16", "I;16B"): - self.check(mode, 2 ** 15 - 1) - self.check(mode, 2 ** 15) - self.check(mode, 2 ** 15 + 1) - self.check(mode, 2 ** 16 - 1) + self.check(mode, 2**15 - 1) + self.check(mode, 2**15) + self.check(mode, 2**15 + 1) + self.check(mode, 2**16 - 1) def test_p_putpixel_rgb_rgba(self): for color in [(255, 0, 0), (255, 0, 0, 255)]: @@ -386,7 +386,7 @@ class TestImagePutPixelError(AccessTest): def test_putpixel_overflow_error(self, mode): im = hopper(mode) with pytest.raises(OverflowError): - im.putpixel((0, 0), 2 ** 80) + im.putpixel((0, 0), 2**80) def test_putpixel_unrecognized_mode(self): im = hopper("BGR;15") diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 7e4bbaaec..3d60e52a2 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,7 +38,7 @@ def test_long_integers(): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2 ** 32: + if sys.maxsize > 2**32: assert put(sys.maxsize) == (255, 255, 255, 255) else: assert put(sys.maxsize) == (255, 255, 255, 127) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index e0093739c..66a72a90e 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -303,7 +303,7 @@ def test_extended_information(): def assert_truncated_tuple_equal(tup1, tup2, digits=10): # Helper function to reduce precision of tuples of floats # recursively and then check equality. - power = 10 ** digits + power = 10**digits def truncate_tuple(tuple_or_float): return tuple( diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 930907939..a42240d49 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -32,10 +32,10 @@ def test_rgb(): def checkrgb(r, g, b): val = ImageQt.rgb(r, g, b) - val = val % 2 ** 24 # drop the alpha + val = val % 2**24 # drop the alpha assert val >> 16 == r - assert ((val >> 8) % 2 ** 8) == g - assert val % 2 ** 8 == b + assert ((val >> 8) % 2**8) == g + assert val % 2**8 == b checkrgb(0, 0, 0) checkrgb(255, 0, 0) diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 9474ff6f9..5717fe150 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -51,8 +51,8 @@ def test_constant(): st = ImageStat.Stat(im) assert st.extrema[0] == (128, 128) - assert st.sum[0] == 128 ** 3 - assert st.sum2[0] == 128 ** 4 + assert st.sum[0] == 128**3 + assert st.sum2[0] == 128**4 assert st.mean[0] == 128 assert st.median[0] == 128 assert st.rms[0] == 128 diff --git a/Tests/test_map.py b/Tests/test_map.py index 42f3447eb..d816bddaf 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -36,7 +36,7 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels -@pytest.mark.skipif(sys.maxsize <= 2 ** 32, reason="Requires 64-bit system") +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/Tests/test_pdfparser.py b/Tests/test_pdfparser.py index 2d428e95f..ea9b33dfc 100644 --- a/Tests/test_pdfparser.py +++ b/Tests/test_pdfparser.py @@ -115,6 +115,6 @@ def test_pdf_repr(): assert pdf_repr(True) == b"true" assert pdf_repr(False) == b"false" assert pdf_repr(None) == b"null" - assert pdf_repr(b"a)/b\\(c") == br"(a\)/b\\\(c)" + assert pdf_repr(b"a)/b\\(c") == rb"(a\)/b\\\(c)" assert pdf_repr([123, True, {"a": PdfName(b"b")}]) == b"[ 123 true <<\n/a /b\n>> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" diff --git a/setup.py b/setup.py index 23d91a5f2..3468b260d 100755 --- a/setup.py +++ b/setup.py @@ -167,7 +167,7 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache args = ["/sbin/ldconfig", "-p"] - expr = fr".*\({abi_type}.*\) => (.*)" + expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 7a7ad386c..72e40b05f 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -102,7 +102,7 @@ class BmpImageFile(ImageFile.ImageFile): file_info["height"] = ( i32(header_data, 4) if not file_info["y_flip"] - else 2 ** 32 - i32(header_data, 4) + else 2**32 - i32(header_data, 4) ) file_info["planes"] = i16(header_data, 8) file_info["bits"] = i16(header_data, 10) @@ -322,7 +322,7 @@ def _save(im, fp, filename, bitmap_header=True): if bitmap_header: offset = 14 + header + colors * 4 file_size = offset + image - if file_size > 2 ** 32 - 1: + if file_size > 2**32 - 1: raise ValueError("File size is too large for the BMP format") fp.write( b"BM" # file type (magic) diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 10fd3ad81..4d7cfbaba 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -38,7 +38,7 @@ class GimpPaletteFile: break # skip fields and comment lines - if re.match(br"\w+:|#", s): + if re.match(rb"\w+:|#", s): continue if len(s) > 100: raise SyntaxError("bad palette file") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 1dfc808c4..f7e690b35 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -100,7 +100,7 @@ for i in range(2, 33): # -------------------------------------------------------------------- # Read IM directory -split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") +split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$") def number(s): diff --git a/src/PIL/ImageStat.py b/src/PIL/ImageStat.py index 50bafc972..ef4a1d633 100644 --- a/src/PIL/ImageStat.py +++ b/src/PIL/ImageStat.py @@ -91,7 +91,7 @@ class Stat: for i in range(0, len(self.h), 256): sum2 = 0.0 for j in range(256): - sum2 += (j ** 2) * float(self.h[i + j]) + sum2 += (j**2) * float(self.h[i + j]) v.append(sum2) return v diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index 21ffd7475..5790acdaf 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -22,7 +22,7 @@ from . import Image, ImageFile # # -------------------------------------------------------------------- -field = re.compile(br"([a-z]*) ([^ \r\n]*)") +field = re.compile(rb"([a-z]*) ([^ \r\n]*)") ## diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index cc7980278..4f4ee8f55 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -132,7 +132,7 @@ def _res_to_dpi(num, denom, exp): calculated as (num / denom) * 10^exp and stored in dots per meter, to floating-point dots per inch.""" if denom != 0: - return (254 * num * (10 ** exp)) / (10000 * denom) + return (254 * num * (10**exp)) / (10000 * denom) def _parse_jp2_header(fp): diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 6ac9c7a7c..9aa0fd6fa 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -576,42 +576,42 @@ class PdfParser: self.xref_table[reference.object_id] = (offset, 0) return reference - delimiter = br"[][()<>{}/%]" - delimiter_or_ws = br"[][()<>{}/%\000\011\012\014\015\040]" - whitespace = br"[\000\011\012\014\015\040]" - whitespace_or_hex = br"[\000\011\012\014\015\0400-9a-fA-F]" + delimiter = rb"[][()<>{}/%]" + delimiter_or_ws = rb"[][()<>{}/%\000\011\012\014\015\040]" + whitespace = rb"[\000\011\012\014\015\040]" + whitespace_or_hex = rb"[\000\011\012\014\015\0400-9a-fA-F]" whitespace_optional = whitespace + b"*" whitespace_mandatory = whitespace + b"+" # No "\012" aka "\n" or "\015" aka "\r": - whitespace_optional_no_nl = br"[\000\011\014\040]*" - newline_only = br"[\r\n]+" + whitespace_optional_no_nl = rb"[\000\011\014\040]*" + newline_only = rb"[\r\n]+" newline = whitespace_optional_no_nl + newline_only + whitespace_optional_no_nl re_trailer_end = re.compile( whitespace_mandatory - + br"trailer" + + rb"trailer" + whitespace_optional - + br"\<\<(.*\>\>)" + + rb"\<\<(.*\>\>)" + newline - + br"startxref" + + rb"startxref" + newline - + br"([0-9]+)" + + rb"([0-9]+)" + newline - + br"%%EOF" + + rb"%%EOF" + whitespace_optional - + br"$", + + rb"$", re.DOTALL, ) re_trailer_prev = re.compile( whitespace_optional - + br"trailer" + + rb"trailer" + whitespace_optional - + br"\<\<(.*?\>\>)" + + rb"\<\<(.*?\>\>)" + newline - + br"startxref" + + rb"startxref" + newline - + br"([0-9]+)" + + rb"([0-9]+)" + newline - + br"%%EOF" + + rb"%%EOF" + whitespace_optional, re.DOTALL, ) @@ -655,12 +655,12 @@ class PdfParser: re_whitespace_optional = re.compile(whitespace_optional) re_name = re.compile( whitespace_optional - + br"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + + rb"/([!-$&'*-.0-;=?-Z\\^-z|~]+)(?=" + delimiter_or_ws - + br")" + + rb")" ) - re_dict_start = re.compile(whitespace_optional + br"\<\<") - re_dict_end = re.compile(whitespace_optional + br"\>\>" + whitespace_optional) + re_dict_start = re.compile(whitespace_optional + rb"\<\<") + re_dict_end = re.compile(whitespace_optional + rb"\>\>" + whitespace_optional) @classmethod def interpret_trailer(cls, trailer_data): @@ -689,7 +689,7 @@ class PdfParser: ) return trailer - re_hashes_in_name = re.compile(br"([^#]*)(#([0-9a-fA-F]{2}))?") + re_hashes_in_name = re.compile(rb"([^#]*)(#([0-9a-fA-F]{2}))?") @classmethod def interpret_name(cls, raw, as_text=False): @@ -704,53 +704,53 @@ class PdfParser: else: return bytes(name) - re_null = re.compile(whitespace_optional + br"null(?=" + delimiter_or_ws + br")") - re_true = re.compile(whitespace_optional + br"true(?=" + delimiter_or_ws + br")") - re_false = re.compile(whitespace_optional + br"false(?=" + delimiter_or_ws + br")") + re_null = re.compile(whitespace_optional + rb"null(?=" + delimiter_or_ws + rb")") + re_true = re.compile(whitespace_optional + rb"true(?=" + delimiter_or_ws + rb")") + re_false = re.compile(whitespace_optional + rb"false(?=" + delimiter_or_ws + rb")") re_int = re.compile( - whitespace_optional + br"([-+]?[0-9]+)(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"([-+]?[0-9]+)(?=" + delimiter_or_ws + rb")" ) re_real = re.compile( whitespace_optional - + br"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + + rb"([-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+))(?=" + delimiter_or_ws - + br")" + + rb")" ) - re_array_start = re.compile(whitespace_optional + br"\[") - re_array_end = re.compile(whitespace_optional + br"]") + re_array_start = re.compile(whitespace_optional + rb"\[") + re_array_end = re.compile(whitespace_optional + rb"]") re_string_hex = re.compile( - whitespace_optional + br"\<(" + whitespace_or_hex + br"*)\>" + whitespace_optional + rb"\<(" + whitespace_or_hex + rb"*)\>" ) - re_string_lit = re.compile(whitespace_optional + br"\(") + re_string_lit = re.compile(whitespace_optional + rb"\(") re_indirect_reference = re.compile( whitespace_optional - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"R(?=" + + rb"R(?=" + delimiter_or_ws - + br")" + + rb")" ) re_indirect_def_start = re.compile( whitespace_optional - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"([-+]?[0-9]+)" + + rb"([-+]?[0-9]+)" + whitespace_mandatory - + br"obj(?=" + + rb"obj(?=" + delimiter_or_ws - + br")" + + rb")" ) re_indirect_def_end = re.compile( - whitespace_optional + br"endobj(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"endobj(?=" + delimiter_or_ws + rb")" ) re_comment = re.compile( - br"(" + whitespace_optional + br"%[^\r\n]*" + newline + br")*" + rb"(" + whitespace_optional + rb"%[^\r\n]*" + newline + rb")*" ) - re_stream_start = re.compile(whitespace_optional + br"stream\r?\n") + re_stream_start = re.compile(whitespace_optional + rb"stream\r?\n") re_stream_end = re.compile( - whitespace_optional + br"endstream(?=" + delimiter_or_ws + br")" + whitespace_optional + rb"endstream(?=" + delimiter_or_ws + rb")" ) @classmethod @@ -876,7 +876,7 @@ class PdfParser: raise PdfFormatError("unrecognized object: " + repr(data[offset : offset + 32])) re_lit_str_token = re.compile( - br"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" + rb"(\\[nrtbf()\\])|(\\[0-9]{1,3})|(\\(\r\n|\r|\n))|(\r\n|\r|\n)|(\()|(\))" ) escaped_chars = { b"n": b"\n", @@ -922,16 +922,16 @@ class PdfParser: offset = m.end() raise PdfFormatError("unfinished literal string") - re_xref_section_start = re.compile(whitespace_optional + br"xref" + newline) + re_xref_section_start = re.compile(whitespace_optional + rb"xref" + newline) re_xref_subsection_start = re.compile( whitespace_optional - + br"([0-9]+)" + + rb"([0-9]+)" + whitespace_mandatory - + br"([0-9]+)" + + rb"([0-9]+)" + whitespace_optional + newline_only ) - re_xref_entry = re.compile(br"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") + re_xref_entry = re.compile(rb"([0-9]{10}) ([0-9]{5}) ([fn])( \r| \n|\r\n)") def read_xref_table(self, xref_section_offset): subsection_found = False diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index cd0a3e0e0..53525e22e 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,7 +48,7 @@ from ._binary import o32be as o32 logger = logging.getLogger(__name__) -is_cid = re.compile(br"\w\w\w\w").match +is_cid = re.compile(rb"\w\w\w\w").match _MAGIC = b"\211PNG\r\n\032\n" diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9d32927d4..9e962cac8 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ class PpmImageFile(ImageFile.ImageFile): if maxval > 255: if not mode == "L": raise ValueError(f"Too many colors for band: {token}") - if maxval < 2 ** 16: + if maxval < 2**16: self.mode = "I" rawmode = "I;16B" else: @@ -136,7 +136,7 @@ def _save(im, fp, filename): elif im.mode == "L": rawmode, head = "L", b"P5" elif im.mode == "I": - if im.getextrema()[1] < 2 ** 16: + if im.getextrema()[1] < 2**16: rawmode, head = "I;16B", b"P5" else: rawmode, head = "I;32B", b"P5" diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 35ff1c1bb..5245e26c6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -577,9 +577,9 @@ class ImageFileDirectory_v2(MutableMapping): else TiffTags.SIGNED_RATIONAL ) elif all(isinstance(v, int) for v in values): - if all(0 <= 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): + elif all(-(2**15) < v < 2**15 for v in values): self.tagtype[tag] = TiffTags.SIGNED_SHORT else: self.tagtype[tag] = ( @@ -734,7 +734,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(5) def write_rational(self, *values): return b"".join( - self._pack("2L", *_limit_rational(frac, 2 ** 32 - 1)) for frac in values + self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @_register_loader(7, 1) @@ -757,7 +757,7 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(10) def write_signed_rational(self, *values): return b"".join( - self._pack("2l", *_limit_signed_rational(frac, 2 ** 31 - 1, -(2 ** 31))) + self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values ) @@ -1670,7 +1670,7 @@ def _save(im, fp, filename): strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip ifd[ROWSPERSTRIP] = rows_per_strip - if strip_byte_counts >= 2 ** 16: + if strip_byte_counts >= 2**16: ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + ( stride * im.size[1] - strip_byte_counts * (strips_per_image - 1), diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 644cfb39b..f7e494485 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -25,7 +25,7 @@ from . import Image, ImageFile # XBM header xbm_head = re.compile( - br"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" + rb"\s*#define[ \t]+.*_width[ \t]+(?P[0-9]+)[\r\n]+" b"#define[ \t]+.*_height[ \t]+(?P[0-9]+)[\r\n]+" b"(?P" b"#define[ \t]+[^_]*_x_hot[ \t]+(?P[0-9]+)[\r\n]+" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 3ff1aeca0..31a3fbab7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -464,7 +464,7 @@ def build_dep_all(): if dep_name in disabled: continue script = build_dep(dep_name) - lines.append(fr'cmd.exe /c "{{build_dir}}\{script}"') + lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) From 954aa4e01dfcb74b98484fad5b739434ff29bdd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 20:04:12 +1100 Subject: [PATCH 87/94] Updated setup-python, checkout and upload-artifact actions to v3 --- .github/workflows/cifuzz.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/test-docker.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- .github/workflows/test-windows.yml | 10 +++++----- .github/workflows/test.yml | 6 +++--- .github/workflows/tidelift.yml | 2 +- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index 7e2fbf28f..0e0abaf95 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -31,13 +31,13 @@ jobs: language: python dry-run: false - name: Upload New Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() && steps.build.outcome == 'success' with: name: artifacts path: ./out/artifacts - name: Upload Legacy Crash - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: steps.run.outcome == 'success' with: name: crash diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 533ce8cbd..4540fb5af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,7 +10,7 @@ jobs: name: Lint steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: pre-commit cache uses: actions/cache@v2 @@ -21,7 +21,7 @@ jobs: lint-pre-commit- - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" cache: pip diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 2762d80c9..f583eae10 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -41,7 +41,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 51bd3a300..7b5cc8a97 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up shell run: echo "C:\msys64\usr\bin\" >> $env:GITHUB_PATH diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 4a8966ca8..21a2b469e 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -28,7 +28,7 @@ jobs: name: ${{ matrix.docker }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build system information run: python3 .github/workflows/system-info.py diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e2cf44cae..64289cc3a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -23,17 +23,17 @@ jobs: steps: - name: Checkout Pillow - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Checkout cached dependencies - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: python-pillow/pillow-depends path: winbuild\depends # sets env: pythonLocation - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -156,7 +156,7 @@ jobs: shell: bash - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors @@ -182,7 +182,7 @@ jobs: winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel shell: cmd - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 133972881..fef442cfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,10 @@ jobs: name: ${{ matrix.os }} Python ${{ matrix.python-version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} cache: pip @@ -84,7 +84,7 @@ jobs: mkdir -p Tests/errors - name: Upload errors - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: errors diff --git a/.github/workflows/tidelift.yml b/.github/workflows/tidelift.yml index c2b8b3bda..2e8c9b730 100644 --- a/.github/workflows/tidelift.yml +++ b/.github/workflows/tidelift.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Scan uses: tidelift/alignment-action@main env: From c8b69a78f3ebf7978be7aacab7209e6bf389acdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 5 Mar 2022 13:42:39 +1100 Subject: [PATCH 88/94] Clip I;16 to be unsigned, not signed --- Tests/test_image_convert.py | 5 +++++ src/libImaging/Convert.c | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 1d6469819..d196f4c78 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -70,6 +70,11 @@ def test_16bit(): with Image.open("Tests/images/16bit.cropped.tif") as im: _test_float_conversion(im) + for color in (65535, 65536): + im = Image.new("I", (1, 1), color) + im_i16 = im.convert("I;16") + assert im_i16.getpixel((0, 0)) == 65535 + def test_16bit_workaround(): with Image.open("Tests/images/16bit.cropped.tif") as im: diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 0f200af6b..96a5a7db1 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -37,7 +37,7 @@ #define MAX(a, b) (a) > (b) ? (a) : (b) #define MIN(a, b) (a) < (b) ? (a) : (b) -#define CLIP16(v) ((v) <= -32768 ? -32768 : (v) >= 32767 ? 32767 : (v)) +#define CLIP16(v) ((v) <= 0 ? 0 : (v) >= 65535 ? 65535 : (v)) /* ITU-R Recommendation 601-2 (assuming nonlinear RGB) */ #define L(rgb) ((INT32)(rgb)[0] * 299 + (INT32)(rgb)[1] * 587 + (INT32)(rgb)[2] * 114) From 633abcbe7e9a1bf955abdd93f71085c55e25fece Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 6 Mar 2022 07:27:11 +1100 Subject: [PATCH 89/94] Updated error message Co-authored-by: Hugo van Kemenade --- src/PIL/FtexImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index b264b7c50..55d28e1ff 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -95,7 +95,7 @@ class FtexImageFile(ImageFile.ImageFile): def _open(self): if not _accept(self.fp.read(4)): - raise SyntaxError("not a FTEX file") + raise SyntaxError("not an FTEX file") struct.unpack(" Date: Mon, 7 Mar 2022 07:15:23 +1100 Subject: [PATCH 90/94] Corrected grammar Co-authored-by: Hugo van Kemenade --- docs/handbook/writing-your-own-image-plugin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59f26d3b2..80138742d 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -354,7 +354,7 @@ Setup The current conventions are that the codec setup function is named ``PyImaging_[codecname]DecoderNew`` or ``PyImaging_[codecname]EncoderNew`` and defined in ``decode.c`` or ``encode.c``. The Python binding for it is -named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is setup from +named ``[codecname]_decoder`` or ``[codecname]_encoder`` and is set up from within the ``_imaging.c`` file in the codecs section of the function array. The setup function needs to call ``PyImaging_DecoderNew`` or @@ -400,7 +400,7 @@ Alternatively, if ``pulls_fd`` or ``pushes_fd`` is set, then the decode or encode function is called once, with an empty buffer. It is the codec's responsibility to transform the entire tile in that one call. Using this will provide a codec with more freedom, but that freedom may mean increased memory -usage if entire tile is held in memory at once by the codec. +usage if the entire tile is held in memory at once by the codec. If an error occurs, set ``state->errcode`` and return -1. From 0d6f3ad6cc9525491aa066ff4f3ae833bd32003e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Mar 2022 19:01:57 +1100 Subject: [PATCH 91/94] Removed load_prepare nearly identical to ImageFile load_prepare --- src/PIL/PsdImagePlugin.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 550a333dd..283219579 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -155,14 +155,6 @@ class PsdImageFile(ImageFile.ImageFile): # return layer number (0=image, 1..max=layers) return self.frame - def load_prepare(self): - # create image memory if necessary - if not self.im or self.im.mode != self.mode or self.im.size != self.size: - self.im = Image.core.fill(self.mode, self.size, 0) - # create palette (optional) - if self.mode == "P": - Image.Image.load(self) - def _close__fp(self): try: if self.__fp != self.fp: From 54a4a38e42d4cecc380628cb47a3421779503690 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Mar 2022 19:48:58 +1100 Subject: [PATCH 92/94] Return -1 when finished decoding --- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/ImageFile.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 779fddea8..ecd3da5df 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -306,7 +306,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder): self._load() except struct.error as e: raise OSError("Truncated BLP file") from e - return 0, 0 + return -1, 0 def _read_blp_header(self): self.fd.seek(4) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7cb4f223b..aac961a76 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -664,7 +664,7 @@ class PyDecoder(PyCodec): :param buffer: A bytes object with the data to be decoded. :returns: A tuple of ``(bytes consumed, errcode)``. - If finished with decoding return 0 for the bytes consumed. + If finished with decoding return -1 for the bytes consumed. Err codes are from :data:`.ImageFile.ERRORS`. """ raise NotImplementedError() diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 32b28d44d..c4d7ddbb4 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -148,7 +148,7 @@ class MspDecoder(ImageFile.PyDecoder): self.set_as_raw(img.getvalue(), ("1", 0, 1)) - return 0, 0 + return -1, 0 Image.register_decoder("MSP", MspDecoder) From e87432b745060dc3ec34119bfe1a198578fd7fa2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Mar 2022 19:51:24 +1100 Subject: [PATCH 93/94] First return value is unused --- src/PIL/ImageFile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index aac961a76..34f344f1d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -231,7 +231,7 @@ class ImageFile(Image.Image): decoder.setimage(self.im, extents) if decoder.pulls_fd: decoder.setfd(self.fp) - status, err_code = decoder.decode(b"") + err_code = decoder.decode(b"")[1] else: b = prefix while True: From 5c1221f070dbf6fa63ed836ae185e23f3a354561 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Mar 2022 07:47:06 +1100 Subject: [PATCH 94/94] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 904a61ce1..6662b5cd5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.1.0 (unreleased) ------------------ +- When converting, clip I;16 to be unsigned, not signed #6112 + [radarhere] + +- Fixed loading L mode GIF with transparency #6086 + [radarhere] + - Improved handling of PPM header #5121 [Piolie, radarhere]