From 700a8e98da37cb586e15fffeb46c04d755950f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:16:44 +1100 Subject: [PATCH 001/155] Support converting between I;16N and L --- Tests/test_mode_i16.py | 11 ++++------- src/libImaging/Convert.c | 7 +++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..134874025 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -90,10 +90,7 @@ def test_convert(): im = original.copy() - verify(im.convert("I;16")) - verify(im.convert("I;16").convert("L")) - verify(im.convert("I;16").convert("I")) - - verify(im.convert("I;16B")) - verify(im.convert("I;16B").convert("L")) - verify(im.convert("I;16B").convert("I")) + for mode in ("I;16", "I;16B", "I;16N"): + verify(im.convert(mode)) + verify(im.convert(mode).convert("L")) + verify(im.convert(mode).convert("I")) diff --git a/src/libImaging/Convert.c b/src/libImaging/Convert.c index 2b45d0cc4..8463e4896 100644 --- a/src/libImaging/Convert.c +++ b/src/libImaging/Convert.c @@ -970,6 +970,13 @@ static struct { {"I;16L", "L", I16L_L}, {"L", "I;16B", L_I16B}, {"I;16B", "L", I16B_L}, +#ifdef WORDS_BIGENDIAN + {"L", "I;16N", L_I16B}, + {"I;16N", "L", I16B_L}, +#else + {"L", "I;16N", L_I16L}, + {"I;16N", "L", I16L_L}, +#endif {"I;16", "F", I16L_F}, {"I;16L", "F", I16L_F}, From e908e10aab5a40caec9988644640382d8e7d2631 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:25:28 +1100 Subject: [PATCH 002/155] Support packing I;16N --- Tests/test_lib_pack.py | 3 +++ src/libImaging/Pack.c | 1 + 2 files changed, 4 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 979806cae..dc5bbb77b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -207,6 +207,9 @@ class TestLibPack: 0x01000083, ) + def test_I16(self): + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + def test_F_float(self): self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) diff --git a/src/libImaging/Pack.c b/src/libImaging/Pack.c index 01760e742..14c8f1461 100644 --- a/src/libImaging/Pack.c +++ b/src/libImaging/Pack.c @@ -664,6 +664,7 @@ static struct { #endif {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, packI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, packI16N_I16}, {"I;16B", "I;16N", 16, packI16N_I16B}, From 8cfc25618f1f66d32dad778be92aac16e34bcc6d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 18:57:24 +1100 Subject: [PATCH 003/155] Support unpacking I;16N --- Tests/test_lib_pack.py | 2 ++ src/libImaging/Unpack.c | 1 + 2 files changed, 3 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index dc5bbb77b..de3e7d156 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -764,10 +764,12 @@ class TestLibUnpack: self.assert_unpack("I;16", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16B", "I;16N", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16L", "I;16N", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) else: self.assert_unpack("I;16", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16N", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16N", 2, 0x0102, 0x0304, 0x0506) + self.assert_unpack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_CMYK16(self): self.assert_unpack("CMYK", "CMYK;16L", 8, (2, 4, 6, 8), (10, 12, 14, 16)) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index e426ed74f..7eeadf944 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1762,6 +1762,7 @@ static struct { {"I;16", "I;16", 16, copy2}, {"I;16B", "I;16B", 16, copy2}, {"I;16L", "I;16L", 16, copy2}, + {"I;16N", "I;16N", 16, copy2}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. From a0e1608f4fe9496dae98e3764969ae92a98ccdf5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 16:55:59 +1100 Subject: [PATCH 004/155] Support accessing I;16N pixels --- Tests/test_image_access.py | 22 ++++++---------------- src/PIL/PyAccess.py | 1 + src/libImaging/Access.c | 9 ++++++++- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6c4f1ceec..d13cff221 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -266,15 +266,10 @@ class TestCffi(AccessTest): # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? self._test_get_access(hopper("F")) - im = Image.new("I;16", (10, 10), 40000) - self._test_get_access(im) - im = Image.new("I;16L", (10, 10), 40000) - self._test_get_access(im) - im = Image.new("I;16B", (10, 10), 40000) - self._test_get_access(im) + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_get_access(im) - im = Image.new("I", (10, 10), 40000) - self._test_get_access(im) # These don't actually appear to be modes that I can actually make, # as unpack sets them directly into the I mode. # im = Image.new('I;32L', (10, 10), -2**10) @@ -313,15 +308,10 @@ class TestCffi(AccessTest): # self._test_set_access(i, (128, 128)) #PA -- undone how to make self._test_set_access(hopper("F"), 1024.0) - im = Image.new("I;16", (10, 10), 40000) - self._test_set_access(im, 45000) - im = Image.new("I;16L", (10, 10), 40000) - self._test_set_access(im, 45000) - im = Image.new("I;16B", (10, 10), 40000) - self._test_set_access(im, 45000) + for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): + im = Image.new(mode, (10, 10), 40000) + self._test_set_access(im, 45000) - im = Image.new("I", (10, 10), 40000) - self._test_set_access(im, 45000) # im = Image.new('I;32L', (10, 10), -(2**10)) # self._test_set_access(im, -(2**13)+1) # im = Image.new('I;32B', (10, 10), 2**10) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 039f5ceea..2043b539c 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -318,6 +318,7 @@ mode_map = { "1": _PyAccess8, "L": _PyAccess8, "P": _PyAccess8, + "I;16N": _PyAccessI16_N, "LA": _PyAccess32_2, "La": _PyAccess32_2, "PA": _PyAccess32_2, diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 83860c38a..f00939da0 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -13,7 +13,7 @@ /* use make_hash.py from the pillow-scripts repository to calculate these values */ #define ACCESS_TABLE_SIZE 27 -#define ACCESS_TABLE_HASH 3078 +#define ACCESS_TABLE_HASH 33051 static struct ImagingAccessInstance access_table[ACCESS_TABLE_SIZE]; @@ -92,6 +92,12 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } +static void +get_pixel_16(Imaging im, int x, int y, void *color) { + UINT8 *in = (UINT8 *)&im->image[y][x + x]; + memcpy(color, in, sizeof(UINT16)); +} + static void get_pixel_32(Imaging im, int x, int y, void *color) { memcpy(color, &im->image32[y][x], sizeof(INT32)); @@ -186,6 +192,7 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); + ADD("I;16N", get_pixel_16, put_pixel_16L); ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); From 97385d7cc7e983c7c1a22bf777152f5ce1dc917c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 2 Jan 2023 19:54:12 +1100 Subject: [PATCH 005/155] Relaxed child images check to allow for libjpeg --- Tests/test_file_jpeg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..fb8954125 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -447,7 +447,7 @@ class TestFileJpeg: ims = im.get_child_images() assert len(ims) == 1 - assert_image_equal_tofile(ims[0], "Tests/images/flower_thumbnail.png") + assert_image_similar_tofile(ims[0], "Tests/images/flower_thumbnail.png", 2.1) def test_mp(self): with Image.open("Tests/images/pil_sample_rgb.jpg") as im: From ea83ebbcf90ffff7b68ffc9ce90aad5c14c10f05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Jan 2023 20:09:47 +1100 Subject: [PATCH 006/155] Moved conversion test to test_imagecms, to skip if lcms2 is absent --- Tests/test_image_convert.py | 11 ----------- Tests/test_imagecms.py | 11 +++++++++++ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 0a7202a33..44d6d1468 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -255,17 +255,6 @@ def test_p2pa_palette(): assert im_pa.getpalette() == im.getpalette() -@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) -def test_rgb_lab(mode): - im = Image.new(mode, (1, 1)) - converted_im = im.convert("LAB") - assert converted_im.getpixel((0, 0)) == (0, 128, 128) - - im = Image.new("LAB", (1, 1), (255, 0, 0)) - converted_im = im.convert(mode) - assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) - - def test_matrix_illegal_conversion(): # Arrange im = hopper("CMYK") diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 3d8dbe6bb..66be02078 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -625,3 +625,14 @@ def test_constants_deprecation(): for name in enum.__members__: with pytest.warns(DeprecationWarning): assert getattr(ImageCms, prefix + name) == enum[name] + + +@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) +def test_rgb_lab(mode): + im = Image.new(mode, (1, 1)) + converted_im = im.convert("LAB") + assert converted_im.getpixel((0, 0)) == (0, 128, 128) + + im = Image.new("LAB", (1, 1), (255, 0, 0)) + converted_im = im.convert(mode) + assert converted_im.getpixel((0, 0))[:3] == (0, 255, 255) From 04cf5e2cfc5dc1676efd9f5c8d605ddfccb0f9ee Mon Sep 17 00:00:00 2001 From: Bas Couwenberg Date: Sat, 14 Jan 2023 19:09:43 +0100 Subject: [PATCH 007/155] Handle more than one directory returned by pkg-config. tiff (4.5.0-1) in Debian results in two include directories being returned: ``` -I/usr/include/x86_64-linux-gnu -I/usr/include ``` --- setup.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 243365681..b4ebbb9c2 100755 --- a/setup.py +++ b/setup.py @@ -263,18 +263,20 @@ def _pkg_config(name): if not DEBUG: command_libs.append("--silence-errors") command_cflags.append("--silence-errors") - libs = ( + libs = re.split( + r"\s*-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") - .strip() - .replace("-L", "") + .strip(), ) - cflags = ( - subprocess.check_output(command_cflags) + libs.remove("") + cflags = re.split( + r"\s*-I", + subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") - .strip() - .replace("-I", "") + .strip(), ) + cflags.remove("") return libs, cflags except Exception: pass @@ -473,8 +475,12 @@ class pil_build_ext(build_ext): else: lib_root = include_root = root - _add_directory(library_dirs, lib_root) - _add_directory(include_dirs, include_root) + if lib_root is not None: + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) + if include_root is not None: + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From 7ad50d9185a7beef755ea7c19b8cfe6e5bea815e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:42:55 -0600 Subject: [PATCH 008/155] log expected & actual color in image access tests --- Tests/test_image_access.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6c4f1ceec..83eb7a1b2 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -139,15 +139,18 @@ class TestImageGetPixel(AccessTest): # check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), c) + d = im.getpixel((0, 0)) assert ( - im.getpixel((0, 0)) == c - ), f"put/getpixel roundtrip failed for mode {mode}, color {c}" + d == c + ), f"put/getpixel roundtrip failed for mode {mode}, expected {c} got {d}" # check putpixel negative index im.putpixel((-1, -1), c) - assert ( - im.getpixel((-1, -1)) == c - ), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}" + d = im.getpixel((-1, -1)) + assert d == c, ( + f"put/getpixel roundtrip negative index failed for mode {mode}, " + f"expected {c} got {d}" + ) # Check 0 im = Image.new(mode, (0, 0), None) @@ -166,13 +169,15 @@ class TestImageGetPixel(AccessTest): # check initial color im = Image.new(mode, (1, 1), c) - assert ( - im.getpixel((0, 0)) == c - ), f"initial color failed for mode {mode}, color {c} " + d = im.getpixel((0, 0)) + assert d == c, f"initial color failed for mode {mode}, expected {c} got {d}" + # check initial color negative index - assert ( - im.getpixel((-1, -1)) == c - ), f"initial color failed with negative index for mode {mode}, color {c} " + d = im.getpixel((-1, -1)) + assert d == c, ( + f"initial color failed with negative index for mode {mode}, " + f"expected {c} got {d}" + ) # Check 0 im = Image.new(mode, (0, 0), c) From 1603872f244da741feeac5a87d235e570723853f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 16 Jan 2023 07:46:11 -0600 Subject: [PATCH 009/155] use better variable names --- Tests/test_image_access.py | 46 +++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 83eb7a1b2..e22b8d74c 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -132,24 +132,25 @@ class TestImageGetPixel(AccessTest): return 1 return tuple(range(1, bands + 1)) - def check(self, mode, c=None): - if not c: - c = self.color(mode) + def check(self, mode, expected_color=None): + if not expected_color: + expected_color = self.color(mode) # check putpixel im = Image.new(mode, (1, 1), None) - im.putpixel((0, 0), c) - d = im.getpixel((0, 0)) - assert ( - d == c - ), f"put/getpixel roundtrip failed for mode {mode}, expected {c} got {d}" + im.putpixel((0, 0), expected_color) + actual_color = im.getpixel((0, 0)) + assert actual_color == expected_color, ( + f"put/getpixel roundtrip failed for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) # check putpixel negative index - im.putpixel((-1, -1), c) - d = im.getpixel((-1, -1)) - assert d == c, ( + im.putpixel((-1, -1), expected_color) + actual_color = im.getpixel((-1, -1)) + assert actual_color == expected_color, ( f"put/getpixel roundtrip negative index failed for mode {mode}, " - f"expected {c} got {d}" + f"expected {expected_color} got {actual_color}" ) # Check 0 @@ -158,29 +159,32 @@ class TestImageGetPixel(AccessTest): error = ValueError if self._need_cffi_access else IndexError with pytest.raises(error): - im.putpixel((0, 0), c) + im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index with pytest.raises(error): - im.putpixel((-1, -1), c) + im.putpixel((-1, -1), expected_color) with pytest.raises(error): im.getpixel((-1, -1)) # check initial color - im = Image.new(mode, (1, 1), c) - d = im.getpixel((0, 0)) - assert d == c, f"initial color failed for mode {mode}, expected {c} got {d}" + im = Image.new(mode, (1, 1), expected_color) + actual_color = im.getpixel((0, 0)) + assert actual_color == expected_color, ( + f"initial color failed for mode {mode}, " + f"expected {expected_color} got {actual_color}" + ) # check initial color negative index - d = im.getpixel((-1, -1)) - assert d == c, ( + actual_color = im.getpixel((-1, -1)) + assert actual_color == expected_color, ( f"initial color failed with negative index for mode {mode}, " - f"expected {c} got {d}" + f"expected {expected_color} got {actual_color}" ) # Check 0 - im = Image.new(mode, (0, 0), c) + im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) # Check 0 negative index From e80707547f67493f565b65319e94a53262914f29 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 16 Jan 2023 07:47:24 -0600 Subject: [PATCH 010/155] parametrize test_image_access::test_signedness() --- Tests/test_image_access.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e22b8d74c..4079d9358 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -214,13 +214,13 @@ class TestImageGetPixel(AccessTest): self.check(mode) @pytest.mark.parametrize("mode", ("I;16", "I;16B")) - def test_signedness(self, mode): + @pytest.mark.parametrize( + "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) + ) + def test_signedness(self, mode, expected_color): # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* - 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, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) From bf0abdca27cd84dafd185bd44206c82b5c14330d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 19 Jan 2023 08:06:30 +1100 Subject: [PATCH 011/155] Do not retry past formats when loading all formats for the first time --- src/PIL/Image.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 7fc8f496e..833473f78 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3268,8 +3268,14 @@ def open(fp, mode="r", formats=None): im = _open_core(fp, filename, prefix, formats) if im is None and formats is ID: + checked_formats = formats.copy() if init(): - im = _open_core(fp, filename, prefix, formats) + im = _open_core( + fp, + filename, + prefix, + tuple(format for format in formats if format not in checked_formats), + ) if im: im._exclusive_fp = exclusive_fp From 9b660db62de0cac22f2d1bf37aabbd412ee7bc62 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 20 Jan 2023 14:35:11 +0200 Subject: [PATCH 012/155] Handling for deprecations to be removed in Pillow 11 --- Tests/test_deprecate.py | 5 +++++ src/PIL/_deprecate.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 30ed4a808..3375eb6b2 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -11,6 +11,11 @@ from PIL import _deprecate "Old thing is deprecated and will be removed in Pillow 10 " r"\(2023-07-01\)\. Use new thing instead\.", ), + ( + 11, + "Old thing is deprecated and will be removed in Pillow 11 " + r"\(2024-10-15\)\. Use new thing instead\.", + ), ( None, r"Old thing is deprecated and will be removed in a future version\. " diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d..fa6e1d00c 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,6 +47,8 @@ def deprecate( raise RuntimeError(msg) elif when == 10: removed = "Pillow 10 (2023-07-01)" + elif when == 11: + removed = "Pillow 11 (2024-10-15)" else: msg = f"Unknown removal version, update {__name__}?" raise ValueError(msg) From e01f5556586a0f789c0ae0fc00d306a2a6513c3b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 22 Jan 2023 06:41:19 +1100 Subject: [PATCH 013/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ed41d46c7..27dbd69bb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Do not retry past formats when loading all formats for the first time #6902 + [radarhere] + +- Do not retry specified formats if they failed when opening #6893 + [radarhere] + +- Do not unintentionally load TIFF format at first #6892 + [radarhere] + - Stop reading when EPS line becomes too long #6897 [radarhere] From 20c54ba1108c831484e2e22c5e143952259a67bb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 23 Jan 2023 07:37:20 +1100 Subject: [PATCH 014/155] Updated libimagequant to 4.1.0 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 541ec8fda..8b847b894 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.0.5 +archive=libimagequant-4.1.0 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 0cea725b4..ea8722c56 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.0.5** + * Pillow has been tested with libimagequant **2.6-4.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 9dc9e82bef9462ee17cdc9c623654a75eab64a6d Mon Sep 17 00:00:00 2001 From: Renat Nasyrov Date: Tue, 24 Jan 2023 00:11:27 +0100 Subject: [PATCH 015/155] Specify correct description for mode L. --- docs/handbook/concepts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 45c662bd6..0aa2f1119 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -31,7 +31,7 @@ INT32 and a 32-bit floating point pixel has the range of FLOAT32. The current re supports the following standard modes: * ``1`` (1-bit pixels, black and white, stored with one pixel per byte) - * ``L`` (8-bit pixels, black and white) + * ``L`` (8-bit pixels, grayscale) * ``P`` (8-bit pixels, mapped to any other mode using a color palette) * ``RGB`` (3x8-bit pixels, true color) * ``RGBA`` (4x8-bit pixels, true color with transparency mask) From e76fa1674e65a38092dcb4b7cbafb2eaaaaaa6c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 14:15:51 +1100 Subject: [PATCH 016/155] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 1fc816146..34609314c 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -6,7 +6,7 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore", category=DeprecationWarning) from PIL import ImageQt -from .helper import assert_image_equal, assert_image_equal_tofile, hopper +from .helper import assert_image_equal_tofile, assert_image_similar, hopper if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_equal(result, expected.convert("RGB")) + assert_image_similar(result, expected.convert("RGB"), 0.3) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 73f55b4e01162c075f9955a5b3eaf86182ddebce Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 1 Jan 2023 19:32:21 +0100 Subject: [PATCH 017/155] remove redundant default value Co-authored-by: Andrew Murray --- Tests/helper.py | 4 ++-- setup.py | 4 +--- src/PIL/ImageFont.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 0d1d03ac8..69246bfcf 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) HAS_UPLOADER = False -if os.environ.get("SHOW_ERRORS", None): +if os.environ.get("SHOW_ERRORS"): # local img.show for errors. HAS_UPLOADER = True @@ -271,7 +271,7 @@ def netpbm_available(): def magick_command(): if sys.platform == "win32": - magickhome = os.environ.get("MAGICK_HOME", "") + magickhome = os.environ.get("MAGICK_HOME") if magickhome: imagemagick = [os.path.join(magickhome, "convert.exe")] graphicsmagick = [os.path.join(magickhome, "gm.exe"), "convert"] diff --git a/setup.py b/setup.py index 243365681..e7f4f476c 100755 --- a/setup.py +++ b/setup.py @@ -567,9 +567,7 @@ class pil_build_ext(build_ext): ): for dirname in _find_library_dirs_ldconfig(): _add_directory(library_dirs, dirname) - if sys.platform.startswith("linux") and os.environ.get( - "ANDROID_ROOT", None - ): + if sys.platform.startswith("linux") and os.environ.get("ANDROID_ROOT"): # termux support for android. # system libraries (zlib) are installed in /system/lib # headers are at $PREFIX/include diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..81e9a640f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -1020,7 +1020,7 @@ def truetype(font=None, size=10, index=0, encoding="", layout_engine=None): if windir: dirs.append(os.path.join(windir, "fonts")) elif sys.platform in ("linux", "linux2"): - lindirs = os.environ.get("XDG_DATA_DIRS", "") + lindirs = os.environ.get("XDG_DATA_DIRS") if not lindirs: # According to the freedesktop spec, XDG_DATA_DIRS should # default to /usr/share From a0492f796876c2a9b8ba445d72c771b84eff93a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 19:27:51 +1100 Subject: [PATCH 018/155] Ensure that pkg-config paths are split by spaces --- setup.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b4ebbb9c2..4382c1a97 100755 --- a/setup.py +++ b/setup.py @@ -264,19 +264,17 @@ def _pkg_config(name): command_libs.append("--silence-errors") command_cflags.append("--silence-errors") libs = re.split( - r"\s*-L", + r"(^|\s+)-L", subprocess.check_output(command_libs, stderr=stderr) .decode("utf8") .strip(), - ) - libs.remove("") + )[::2][1:] cflags = re.split( - r"\s*-I", + r"(^|\s+)-I", subprocess.check_output(command_cflags, stderr=stderr) .decode("utf8") .strip(), - ) - cflags.remove("") + )[::2][1:] return libs, cflags except Exception: pass From 3e37a919b136d35447bde7694ecf579a2096b163 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 22:43:04 +1100 Subject: [PATCH 019/155] Prevent register_open from adding duplicates to ID --- Tests/test_image.py | 11 +++++++++++ src/PIL/Image.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index d261638f9..ad3346b5a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -398,6 +398,17 @@ class TestImage: with pytest.raises(ValueError): source.alpha_composite(over, (0, 0), (0, -1)) + def test_register_open_duplicates(self): + # Arrange + factory, accept = Image.OPEN["JPEG"] + id_length = len(Image.ID) + + # Act + Image.register_open("JPEG", factory, accept) + + # Assert + assert len(Image.ID) == id_length + def test_registered_extensions_uninitialized(self): # Arrange Image._initialized = 0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 833473f78..ad0d25add 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3406,7 +3406,8 @@ def register_open(id, factory, accept=None): reject images having another format. """ id = id.upper() - ID.append(id) + if id not in ID: + ID.append(id) OPEN[id] = factory, accept From 510de501ead4fab2d85e84f2f48ec98438dc0c9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 17:18:17 +1100 Subject: [PATCH 020/155] Moved test_get_child_images to test_file_libtiff.py --- Tests/test_file_libtiff.py | 18 ++++++++++++++++++ Tests/test_file_tiff.py | 18 ------------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..6e111ebfc 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1067,3 +1067,21 @@ class TestFileLibTiff(LibTiffTestCase): out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) + + @pytest.mark.parametrize( + "path, sizes", + ( + ("Tests/images/hopper.tif", ()), + ("Tests/images/child_ifd.tiff", (16, 8)), + ("Tests/images/child_ifd_jpeg.tiff", (20,)), + ), + ) + def test_get_child_images(self, path, sizes): + with Image.open(path) as im: + ims = im.get_child_images() + + assert len(ims) == len(sizes) + for i, im in enumerate(ims): + w = sizes[i] + expected = Image.new("RGB", (w, w), "#f00") + assert_image_similar(im, expected, 1) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..96db4cb5e 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -84,24 +84,6 @@ class TestFileTiff: with Image.open("Tests/images/multipage.tiff") as im: im.load() - @pytest.mark.parametrize( - "path, sizes", - ( - ("Tests/images/hopper.tif", ()), - ("Tests/images/child_ifd.tiff", (16, 8)), - ("Tests/images/child_ifd_jpeg.tiff", (20,)), - ), - ) - def test_get_child_images(self, path, sizes): - with Image.open(path) as im: - ims = im.get_child_images() - - assert len(ims) == len(sizes) - for i, im in enumerate(ims): - w = sizes[i] - expected = Image.new("RGB", (w, w), "#f00") - assert_image_similar(im, expected, 1) - def test_mac_tiff(self): # Read RGBa images from macOS [@PIL136] From 412f5a523356f8e5618aadd1c5ba1e2dd62052a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 17:29:36 +1100 Subject: [PATCH 021/155] Moved test_wrong_bits_per_sample to test_file_libtiff.py --- Tests/test_file_libtiff.py | 30 ++++++++++++++++++++++++++++++ Tests/test_file_tiff.py | 30 ------------------------------ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 6e111ebfc..26feeb540 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -986,6 +986,36 @@ class TestFileLibTiff(LibTiffTestCase): ) as im: assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") + @pytest.mark.parametrize( + "file_name,mode,size,tile", + [ + ( + "tiff_wrong_bits_per_sample.tiff", + "RGBA", + (52, 53), + [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_2.tiff", + "RGB", + (16, 16), + [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], + ), + ( + "tiff_wrong_bits_per_sample_3.tiff", + "RGBA", + (512, 256), + [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], + ), + ], + ) + def test_wrong_bits_per_sample(self, file_name, mode, size, tile): + with Image.open("Tests/images/" + file_name) as im: + assert im.mode == mode + assert im.size == size + assert im.tile == tile + im.load() + def test_no_rows_per_strip(self): # This image does not have a RowsPerStrip TIFF tag infile = "Tests/images/no_rows_per_strip.tif" diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 96db4cb5e..cd8a00ab0 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -100,36 +100,6 @@ class TestFileTiff: with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") - @pytest.mark.parametrize( - "file_name,mode,size,tile", - [ - ( - "tiff_wrong_bits_per_sample.tiff", - "RGBA", - (52, 53), - [("raw", (0, 0, 52, 53), 160, ("RGBA", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_2.tiff", - "RGB", - (16, 16), - [("raw", (0, 0, 16, 16), 8, ("RGB", 0, 1))], - ), - ( - "tiff_wrong_bits_per_sample_3.tiff", - "RGBA", - (512, 256), - [("libtiff", (0, 0, 512, 256), 0, ("RGBA", "tiff_lzw", False, 48782))], - ), - ], - ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile): - with Image.open("Tests/images/" + file_name) as im: - assert im.mode == mode - assert im.size == size - assert im.tile == tile - im.load() - def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: From 1de6c958dfedbe9762266d73559dfc1635b7744c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 18:43:40 +1100 Subject: [PATCH 022/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 27dbd69bb..e35a55965 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Handle more than one directory returned by pkg-config #6896 + [sebastic, radarhere] + - Do not retry past formats when loading all formats for the first time #6902 [radarhere] From 446cfddb5d11ae678ccb7a8aac4c948b9555fd3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 20:05:35 +1100 Subject: [PATCH 023/155] pre-commit autoupdate --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d790e7850..5214d352d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: types: [] - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort @@ -26,7 +26,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.3.1 + rev: v1.4.2 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) @@ -39,7 +39,7 @@ repos: [flake8-2020, flake8-errmsg, flake8-implicit-str-concat] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - id: rst-backticks @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.5.2 + rev: 0.6.1 hooks: - id: tox-ini-fmt From 9932d0cb5ccc9bfc49ee4c7383f215fec06e4b6a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 024/155] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From c8966013bd47fe59f729969463360f12763641c4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 27 Jan 2023 23:19:03 +0200 Subject: [PATCH 025/155] Replace SVN with Git for installing extra test images --- .appveyor.yml | 4 +++- .github/workflows/test-windows.yml | 5 ++++- depends/install_extra_test_images.sh | 21 +++++++++------------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b817cd9d8..d4dd2dc95 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -21,9 +21,11 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' - curl -fsSL -o pillow-depends.zip https://github.com/python-pillow/pillow-depends/archive/main.zip +- curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-depends.zip -oc:\ +- 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends -- xcopy /S /Y c:\pillow-depends\test_images\* c:\pillow\tests\images +- xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images - 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ - ..\pillow-depends\gs1000w32.exe /S - path c:\nasm-2.15.05;C:\Program Files (x86)\gs\gs10.0.0\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 487c3586f..48825dc30 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -62,7 +62,10 @@ jobs: winbuild\depends\gs1000w32.exe /S echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH - xcopy /S /Y winbuild\depends\test_images\* Tests\images\ + # Install extra test images + curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + 7z x pillow-test-images.zip -oc:\ + xcopy /S /Y c:\test-images-main\* Tests\images\ # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 02da12d61..7381d3767 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -1,15 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # install extra test images -# Use SVN to just fetch a single Git subdirectory -svn_export() -{ - if [ ! -z $1 ]; then - echo "" - echo "Retrying svn export..." - echo "" - fi +archive=test-images-main - svn export --force https://github.com/python-pillow/pillow-depends/trunk/test_images ../Tests/images -} -svn_export || svn_export retry || svn_export retry || svn_export retry +./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz + +mv $archive/* ../Tests/images/ + +# Cleanup old tarball and empty directory +rm $archive.tar.gz +rm -r $archive From 120d56b4ba49871a6d3032b7d0d4c8159e8273ec Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:11 +0200 Subject: [PATCH 026/155] Sort dependencies --- .github/workflows/test-cygwin.yml | 28 ++++++++++++++++++++++------ .github/workflows/test-mingw.yml | 10 +++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 7b8070d34..1dfb36f44 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -34,18 +34,34 @@ jobs: with: platform: x86_64 packages: > - ImageMagick gcc-g++ ghostscript jpeg libfreetype-devel - libimagequant-devel libjpeg-devel liblapack-devel - liblcms2-devel libopenjp2-devel libraqm-devel - libtiff-devel libwebp-devel libxcb-devel libxcb-xinerama0 - make netpbm perl + gcc-g++ + ghostscript + ImageMagick + jpeg + libfreetype-devel + libimagequant-devel + libjpeg-devel + liblapack-devel + liblcms2-devel + libopenjp2-devel + libraqm-devel + libtiff-devel + libwebp-devel + libxcb-devel + libxcb-xinerama0 + make + netpbm + perl python3${{ matrix.python-minor-version }}-cffi python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools subversion xorg-server-extra zlib-devel + qt5-devel-tools + subversion + xorg-server-extra + zlib-devel - name: Add Lapack to PATH uses: egor-tensin/cleanup-path@v3 diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 6a60bc7f0..24575f6c7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -45,11 +45,6 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-python3-cffi \ - ${{ matrix.package }}-python3-numpy \ - ${{ matrix.package }}-python3-olefile \ - ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ ${{ matrix.package }}-freetype \ ${{ matrix.package }}-gcc \ ${{ matrix.package }}-ghostscript \ @@ -60,6 +55,11 @@ jobs: ${{ matrix.package }}-libtiff \ ${{ matrix.package }}-libwebp \ ${{ matrix.package }}-openjpeg2 \ + ${{ matrix.package }}-python3-cffi \ + ${{ matrix.package }}-python3-numpy \ + ${{ matrix.package }}-python3-olefile \ + ${{ matrix.package }}-python3-pip \ + ${{ matrix.package }}-python3-setuptools \ subversion if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then From 7e35e15eeeca12a4eec61d39ff7fc819d08d3554 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 28 Jan 2023 16:40:25 +0200 Subject: [PATCH 027/155] Replace subversion with wget package --- .github/workflows/test-cygwin.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 1dfb36f44..451181434 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -59,7 +59,7 @@ jobs: python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter qt5-devel-tools - subversion + wget xorg-server-extra zlib-devel diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 24575f6c7..ef8214649 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -60,7 +60,7 @@ jobs: ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ ${{ matrix.package }}-python3-setuptools \ - subversion + ${{ matrix.package }}-wget if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From 772567a4ce9ca1890ecbee976bb777e82db2577a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 20:13:28 +1100 Subject: [PATCH 028/155] Switched to python-pillow repositories --- .github/workflows/test-windows.yml | 2 +- depends/install_extra_test_images.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 48825dc30..cf160a997 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -63,7 +63,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/hugovk/test-images/archive/main.zip + curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip 7z x pillow-test-images.zip -oc:\ xcopy /S /Y c:\test-images-main\* Tests\images\ diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 7381d3767..ffdfe17f2 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/hugovk/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz mv $archive/* ../Tests/images/ From a119b19c074869267f186051af0d1b0878f9cca0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Feb 2023 08:36:06 +1100 Subject: [PATCH 029/155] Updated libjpeg-turbo to 2.1.5 --- 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 fd12240e5..89903c621 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.4/libjpeg-turbo-2.1.4.tar.gz/download", - "filename": "libjpeg-turbo-2.1.4.tar.gz", - "dir": "libjpeg-turbo-2.1.4", + + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.tar.gz", + "dir": "libjpeg-turbo-2.1.5", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From b3af769c1a85e62600d6bbf38a9e66639a60d43c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Feb 2023 20:52:50 +1100 Subject: [PATCH 030/155] Set alpha channel for OpenJPEG --- src/libImaging/Jpeg2KEncode.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c9..62d22bcc6 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -487,6 +487,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { goto quick_exit; } + if (strcmp(im->mode, "RGBA") == 0) { + image->comps[3].alpha = 1; + } + opj_set_error_handler(codec, j2k_error, context); opj_set_info_handler(codec, j2k_warn, context); opj_set_warning_handler(codec, j2k_warn, context); From 642d57408769ec28694c2c6acc6bf8ca433977fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Feb 2023 21:48:47 +1100 Subject: [PATCH 031/155] Added JPXDecode for RGBA images --- Tests/test_file_pdf.py | 7 ++++++- src/PIL/PdfImagePlugin.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..705505e83 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -8,7 +8,7 @@ import pytest from PIL import Image, PdfParser, features -from .helper import hopper, mark_if_feature_version +from .helper import hopper, mark_if_feature_version, skip_unless_feature def helper_save_as_pdf(tmp_path, mode, **kwargs): @@ -42,6 +42,11 @@ def test_save(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) +@skip_unless_feature("jpg_2000") +def test_save_rgba(tmp_path): + helper_save_as_pdf(tmp_path, "RGBA") + + def test_monochrome(tmp_path): # Arrange mode = "1" diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..0b2938d6f 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -168,6 +168,10 @@ def _save(im, fp, filename, save_all=False): filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + colorspace = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images elif im.mode == "CMYK": filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") @@ -194,6 +198,8 @@ def _save(im, fp, filename, save_all=False): ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + Image.SAVE["JPEG2000"](im, op, filename) elif filter == "FlateDecode": ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) elif filter == "RunLengthDecode": From cc71b4c1b2226330c77210f2243deaa8b254afdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Feb 2023 10:04:29 +0200 Subject: [PATCH 032/155] Simpler URL Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index ffdfe17f2..941bfbe84 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -3,7 +3,7 @@ archive=test-images-main -./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/refs/heads/main.tar.gz +./download-and-extract.sh $archive https://github.com/python-pillow/test-images/archive/main.tar.gz mv $archive/* ../Tests/images/ From cb4eb0d40ff0f9b6a6f3deb2c299ed477c1afc00 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:25:53 +0000 Subject: [PATCH 033/155] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5214d352d..45c1f3c5f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.12.0 + rev: 23.1.0 hooks: - id: black args: [--target-version=py37] From 24183d652e02404e1586ff00f7fe4a055b05ce58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Feb 2023 19:27:15 +0000 Subject: [PATCH 034/155] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/check_fli_overflow.py | 1 - Tests/check_png_dos.py | 1 - Tests/test_bmp_reference.py | 1 - Tests/test_file_bmp.py | 1 - Tests/test_file_bufrstub.py | 2 -- Tests/test_file_dcx.py | 2 -- Tests/test_file_eps.py | 1 - Tests/test_file_fits.py | 1 - Tests/test_file_fli.py | 2 -- Tests/test_file_gif.py | 13 +------------ Tests/test_file_gribstub.py | 2 -- Tests/test_file_hdf5stub.py | 2 -- Tests/test_file_icns.py | 1 - Tests/test_file_ico.py | 1 - Tests/test_file_im.py | 1 - Tests/test_file_iptc.py | 3 --- Tests/test_file_jpeg.py | 13 ------------- Tests/test_file_jpeg2k.py | 1 - Tests/test_file_libtiff.py | 2 -- Tests/test_file_msp.py | 2 -- Tests/test_file_pdf.py | 2 -- Tests/test_file_png.py | 4 ---- Tests/test_file_psd.py | 2 -- Tests/test_file_spider.py | 1 - Tests/test_file_sun.py | 1 - Tests/test_file_tga.py | 3 --- Tests/test_file_tiff.py | 7 ------- Tests/test_file_tiff_metadata.py | 3 --- Tests/test_file_webp_metadata.py | 4 ---- Tests/test_file_wmf.py | 1 - Tests/test_file_xbm.py | 2 -- Tests/test_file_xvthumb.py | 1 - Tests/test_image.py | 2 -- Tests/test_image_convert.py | 1 - Tests/test_image_crop.py | 1 - Tests/test_image_getbbox.py | 1 - Tests/test_image_mode.py | 1 - Tests/test_image_rotate.py | 2 +- Tests/test_image_tobitmap.py | 1 - Tests/test_imagechops.py | 21 --------------------- Tests/test_imagedraw.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 3 --- Tests/test_imagepalette.py | 3 --- Tests/test_imagepath.py | 1 - Tests/test_imagesequence.py | 1 - Tests/test_imagestat.py | 3 --- Tests/test_lib_image.py | 1 - Tests/test_mode_i16.py | 2 -- Tests/test_numpy.py | 1 - Tests/test_pickle.py | 1 - Tests/test_tiff_ifdrational.py | 2 -- Tests/test_webp_leaks.py | 1 - setup.py | 2 -- src/PIL/BmpImagePlugin.py | 2 -- src/PIL/BufrStubImagePlugin.py | 2 -- src/PIL/CurImagePlugin.py | 2 -- src/PIL/DcxImagePlugin.py | 2 -- src/PIL/EpsImagePlugin.py | 2 -- src/PIL/FitsImagePlugin.py | 1 - src/PIL/FitsStubImagePlugin.py | 1 - src/PIL/FliImagePlugin.py | 2 -- src/PIL/FontFile.py | 1 - src/PIL/FpxImagePlugin.py | 5 ----- src/PIL/GbrImagePlugin.py | 1 - src/PIL/GdImageFile.py | 1 - src/PIL/GifImagePlugin.py | 6 ------ src/PIL/GimpGradientFile.py | 5 ----- src/PIL/GimpPaletteFile.py | 3 --- src/PIL/GribStubImagePlugin.py | 2 -- src/PIL/Hdf5StubImagePlugin.py | 2 -- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 7 ------- src/PIL/Image.py | 3 +-- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageFile.py | 3 --- src/PIL/ImageFont.py | 2 -- src/PIL/ImageOps.py | 2 -- src/PIL/ImagePalette.py | 2 -- src/PIL/ImageShow.py | 1 - src/PIL/ImageTk.py | 2 -- src/PIL/ImtImagePlugin.py | 5 ----- src/PIL/IptcImagePlugin.py | 3 --- src/PIL/JpegImagePlugin.py | 5 ----- src/PIL/McIdasImagePlugin.py | 2 -- src/PIL/MicImagePlugin.py | 2 -- src/PIL/MpegImagePlugin.py | 2 -- src/PIL/MpoImagePlugin.py | 1 - src/PIL/MspImagePlugin.py | 4 ---- src/PIL/PaletteFile.py | 3 --- src/PIL/PalmImagePlugin.py | 4 ---- src/PIL/PcdImagePlugin.py | 2 -- src/PIL/PcfFontFile.py | 7 ------- src/PIL/PcxImagePlugin.py | 3 --- src/PIL/PixarImagePlugin.py | 2 -- src/PIL/PngImagePlugin.py | 17 ----------------- src/PIL/PpmImagePlugin.py | 1 - src/PIL/PsdImagePlugin.py | 4 ---- src/PIL/SgiImagePlugin.py | 2 -- src/PIL/SpiderImagePlugin.py | 3 +-- src/PIL/SunImagePlugin.py | 2 -- src/PIL/TarIO.py | 1 - src/PIL/TgaImagePlugin.py | 3 --- src/PIL/TiffImagePlugin.py | 4 ---- src/PIL/WalImageFile.py | 1 - src/PIL/WebPImagePlugin.py | 1 - src/PIL/WmfImagePlugin.py | 2 -- src/PIL/XVThumbImagePlugin.py | 2 -- src/PIL/XbmImagePlugin.py | 3 --- src/PIL/XpmImagePlugin.py | 7 ------- 111 files changed, 8 insertions(+), 298 deletions(-) diff --git a/Tests/check_fli_overflow.py b/Tests/check_fli_overflow.py index 08a55d349..c600c45ed 100644 --- a/Tests/check_fli_overflow.py +++ b/Tests/check_fli_overflow.py @@ -4,7 +4,6 @@ TEST_FILE = "Tests/images/fli_overflow.fli" def test_fli_overflow(): - # this should not crash with a malloc error or access violation with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/check_png_dos.py b/Tests/check_png_dos.py index d8d645189..f4a129f50 100644 --- a/Tests/check_png_dos.py +++ b/Tests/check_png_dos.py @@ -23,7 +23,6 @@ def test_ignore_dos_text(): def test_dos_text(): - try: im = Image.open(TEST_FILE) im.load() diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ed9aff9cc..002a44a4f 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -18,7 +18,6 @@ def test_bad(): """These shouldn't crash/dos, but they shouldn't return anything either""" for f in get_files("b"): - # Assert that there is no unclosed file warning with warnings.catch_warnings(): try: diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 5f6d52355..9e79937e9 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -141,7 +141,6 @@ def test_rgba_bitfields(): # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: - # So before the comparing the image, swap the channels b, g, r = im.split()[1:] im = Image.merge("RGB", (r, g, b)) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index e330404d6..76f185b9a 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/gfs.t06z.rassda.tm00.bufr_d" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "BUFR" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 0f09c4b99..ef378b24a 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -15,7 +15,6 @@ def test_sanity(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.size == (128, 128) assert isinstance(im, DcxImagePlugin.DcxImageFile) @@ -54,7 +53,6 @@ def test_invalid_file(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..ac6e84447 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -80,7 +80,6 @@ def test_invalid_file(): @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_cmyk(): with Image.open("Tests/images/pil_sample_cmyk.eps") as cmyk_image: - assert cmyk_image.mode == "CMYK" assert cmyk_image.size == (100, 100) assert cmyk_image.format == "EPS" diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 447888acd..d2f5a6d17 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -12,7 +12,6 @@ TEST_FILE = "Tests/images/hopper.fits" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "FITS" assert im.size == (128, 128) diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index b8b999d70..70d4d76db 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -64,7 +64,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(static_test_file) as im: - # Act frame = im.tell() @@ -110,7 +109,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(animated_test_file) as im: - layer_number = im.tell() assert layer_number == 0 diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 6fbc0ee30..bce72d192 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -209,7 +209,7 @@ def test_optimize_if_palette_can_be_reduced_by_half(): im = im.resize((591, 443)) im_rgb = im.convert("RGB") - for (optimize, colors) in ((False, 256), (True, 8)): + for optimize, colors in ((False, 256), (True, 8)): out = BytesIO() im_rgb.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: @@ -221,7 +221,6 @@ def test_roundtrip(tmp_path): im = hopper() im.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) @@ -232,7 +231,6 @@ def test_roundtrip2(tmp_path): im2 = im.copy() im2.save(out) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), hopper(), 50) @@ -242,7 +240,6 @@ def test_roundtrip_save_all(tmp_path): im = hopper() im.save(out, save_all=True) with Image.open(out) as reread: - assert_image_similar(reread.convert("RGB"), im, 50) # Multiframe image @@ -284,13 +281,11 @@ def test_headers_saving_for_animated_gifs(tmp_path): important_headers = ["background", "version", "duration", "loop"] # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - info = im.info.copy() out = str(tmp_path / "temp.gif") im.save(out, save_all=True) with Image.open(out) as reread: - for header in important_headers: assert info[header] == reread.info[header] @@ -308,7 +303,6 @@ def test_palette_handling(tmp_path): im2.save(f, optimize=True) with Image.open(f) as reloaded: - assert_image_similar(im, reloaded.convert("RGB"), 10) @@ -324,7 +318,6 @@ def test_palette_434(tmp_path): orig = "Tests/images/test.colors.gif" with Image.open(orig) as im: - with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -575,7 +568,6 @@ def test_save_dispose(tmp_path): ) with Image.open(out) as img: - for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -773,7 +765,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -786,7 +777,6 @@ def test_multiple_duration(tmp_path): out, save_all=True, append_images=im_list[1:], duration=tuple(duration_list) ) with Image.open(out) as reread: - for duration in duration_list: assert reread.info["duration"] == duration try: @@ -844,7 +834,6 @@ def test_identical_frames(tmp_path): out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: - # Assert that the first three frames were combined assert reread.n_frames == 2 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index fd427746e..768ac12bd 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/WAlaska.wind.7days.grb" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "GRIB" @@ -31,7 +30,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 20b4b9619..98dc5443c 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -8,7 +8,6 @@ TEST_FILE = "Tests/images/hdf5.h5" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "HDF5" @@ -29,7 +28,6 @@ def test_invalid_file(): def test_load(): # Arrange with Image.open(TEST_FILE) as im: - # Act / Assert: stub cannot load without an implemented handler with pytest.raises(OSError): im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 55632909c..42275424d 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -16,7 +16,6 @@ def test_sanity(): # Loading this icon by default should result in the largest size # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: - # Assert that there is no unclosed file warning with warnings.catch_warnings(): im.load() diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index afb17b1af..9c1c3cf17 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -175,7 +175,6 @@ def test_save_256x256(tmp_path): # Act im.save(outfile) with Image.open(outfile) as im_saved: - # Assert assert im_saved.size == (256, 256) diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 5cf93713b..425e690d6 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -51,7 +51,6 @@ def test_context_manager(): def test_tell(): # Arrange with Image.open(TEST_IM) as im: - # Act frame = im.tell() diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index 2d0e6977a..2d99528d3 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -11,7 +11,6 @@ TEST_FILE = "Tests/images/iptc.jpg" def test_getiptcinfo_jpg_none(): # Arrange with hopper() as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -22,7 +21,6 @@ def test_getiptcinfo_jpg_none(): def test_getiptcinfo_jpg_found(): # Arrange with Image.open(TEST_FILE) as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) @@ -35,7 +33,6 @@ def test_getiptcinfo_jpg_found(): def test_getiptcinfo_tiff_none(): # Arrange with Image.open("Tests/images/hopper.tif") as im: - # Act iptc = IptcImagePlugin.getiptcinfo(im) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index eabc6bf75..e3c5abcbd 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -57,7 +57,6 @@ class TestFileJpeg: return Image.frombytes(mode, size, os.urandom(size[0] * size[1] * len(mode))) def test_sanity(self): - # internal version number assert re.search(r"\d+\.\d+$", features.version_codec("jpg")) @@ -368,7 +367,6 @@ class TestFileJpeg: def test_exif_gps_typeerror(self): with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: - # Should not raise a TypeError im._getexif() @@ -682,7 +680,6 @@ class TestFileJpeg: # Shouldn't raise error fn = "Tests/images/sugarshack_bad_mpo_header.jpg" with pytest.warns(UserWarning, Image.open, fn) as im: - # Assert assert im.format == "JPEG" @@ -704,7 +701,6 @@ class TestFileJpeg: # Arrange outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/hopper.tif") as im: - # Act im.save(outfile, "JPEG", dpi=im.info["dpi"]) @@ -731,7 +727,6 @@ class TestFileJpeg: # This Photoshop CC 2017 image has DPI in EXIF not metadata # EXIF XResolution is (2000000, 10000) with Image.open("Tests/images/photoshop-200dpi.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (200, 200) @@ -740,7 +735,6 @@ class TestFileJpeg: # This image has DPI in EXIF not metadata # EXIF XResolution is 72 with Image.open("Tests/images/exif-72dpi-int.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (72, 72) @@ -749,7 +743,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution unit set to cm: # exiftool -exif:ResolutionUnit=cm photoshop-200dpi.jpg with Image.open("Tests/images/exif-200dpcm.jpg") as im: - # Act / Assert assert im.info.get("dpi") == (508, 508) @@ -758,7 +751,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with EXIF resolution set to 0/0: # exiftool -XResolution=0/0 -YResolution=0/0 photoshop-200dpi.jpg with Image.open("Tests/images/exif-dpi-zerodivision.jpg") as im: - # Act / Assert # This should return the default, and not raise a ZeroDivisionError assert im.info.get("dpi") == (72, 72) @@ -767,7 +759,6 @@ class TestFileJpeg: # Arrange # 0x011A tag in this exif contains string '300300\x02' with Image.open("Tests/images/broken_exif_dpi.jpg") as im: - # Act / Assert # This should return the default assert im.info.get("dpi") == (72, 72) @@ -777,7 +768,6 @@ class TestFileJpeg: # This is photoshop-200dpi.jpg with resolution removed from EXIF: # exiftool "-*resolution*"= photoshop-200dpi.jpg with Image.open("Tests/images/no-dpi-in-exif.jpg") as im: - # Act / Assert # "When the image resolution is unknown, 72 [dpi] is designated." # https://exiv2.org/tags.html @@ -787,7 +777,6 @@ class TestFileJpeg: # This is no-dpi-in-exif with the tiff header of the exif block # hexedited from MM * to FF FF FF FF with Image.open("Tests/images/invalid-exif.jpg") as im: - # This should return the default, and not a SyntaxError or # OSError for unidentified image. assert im.info.get("dpi") == (72, 72) @@ -810,7 +799,6 @@ class TestFileJpeg: def test_invalid_exif_x_resolution(self): # When no x or y resolution is defined in EXIF with Image.open("Tests/images/invalid-exif-without-x-resolution.jpg") as im: - # This should return the default, and not a ValueError or # OSError for an unidentified image. assert im.info.get("dpi") == (72, 72) @@ -820,7 +808,6 @@ class TestFileJpeg: # This image has been manually hexedited to have an IFD offset of 10, # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: - # Act / Assert assert im._getexif()[306] == "2017:03:13 23:03:09" diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..de622c478 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -270,7 +270,6 @@ def test_rgba(): # Arrange with Image.open("Tests/images/rgb_trns_ycbc.j2k") as j2k: with Image.open("Tests/images/rgb_trns_ycbc.jp2") as jp2: - # Act j2k.load() jp2.load() diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1109cd15e..f886d3aae 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -645,7 +645,6 @@ class TestFileLibTiff(LibTiffTestCase): pilim = hopper() def save_bytesio(compression=None): - buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -740,7 +739,6 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self): with Image.open("Tests/images/compression.tif") as im: - im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) diff --git a/Tests/test_file_msp.py b/Tests/test_file_msp.py index 50d7c590b..497052b05 100644 --- a/Tests/test_file_msp.py +++ b/Tests/test_file_msp.py @@ -44,7 +44,6 @@ def test_open_windows_v1(): # Arrange # Act with Image.open(TEST_FILE) as im: - # Assert assert_image_equal(im, hopper("1")) assert isinstance(im, MspImagePlugin.MspImageFile) @@ -59,7 +58,6 @@ def _assert_file_image_equal(source_path, target_path): not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) def test_open_windows_v2(): - files = ( os.path.join(EXTRA_DIR, f) for f in os.listdir(EXTRA_DIR) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 5299febe9..216b93ca9 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -89,7 +89,6 @@ def test_save_all(tmp_path): # Multiframe image with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile, save_all=True) @@ -123,7 +122,6 @@ def test_save_all(tmp_path): def test_multiframe_normal_save(tmp_path): # Test saving a multiframe image without save_all with Image.open("Tests/images/dispose_bgnd.gif") as im: - outfile = str(tmp_path / "temp.pdf") im.save(outfile) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 133f3e47e..c4db97905 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -78,7 +78,6 @@ class TestFilePng: return chunks def test_sanity(self, tmp_path): - # internal version number assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) @@ -156,7 +155,6 @@ class TestFilePng: assert im.info == {"spam": "egg"} def test_bad_itxt(self): - im = load(HEAD + chunk(b"iTXt") + TAIL) assert im.info == {} @@ -201,7 +199,6 @@ class TestFilePng: assert im.info["spam"].tkey == "Spam" def test_interlace(self): - test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) @@ -495,7 +492,6 @@ class TestFilePng: # Check reading images with null tRNS value, issue #1239 test_file = "Tests/images/tRNS_null_1x1.png" with Image.open(test_file) as im: - assert im.info["transparency"] == 0 def test_save_icc_profile(self): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 4f934375c..036cb9d4b 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -77,7 +77,6 @@ def test_eoferror(): def test_seek_tell(): with Image.open(test_file) as im: - layer_number = im.tell() assert layer_number == 1 @@ -95,7 +94,6 @@ def test_seek_tell(): def test_seek_eoferror(): with Image.open(test_file) as im: - with pytest.raises(EOFError): im.seek(-1) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 0e3b705a2..011e208d8 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -79,7 +79,6 @@ def test_is_spider_image(): def test_tell(): # Arrange with Image.open(TEST_FILE) as im: - # Act index = im.tell() diff --git a/Tests/test_file_sun.py b/Tests/test_file_sun.py index 05c78c316..edb320603 100644 --- a/Tests/test_file_sun.py +++ b/Tests/test_file_sun.py @@ -16,7 +16,6 @@ def test_sanity(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (128, 128) diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 7d8b5139a..bac00e855 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -78,7 +78,6 @@ def test_id_field(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (100, 100) @@ -89,7 +88,6 @@ def test_id_field_rle(): # Act with Image.open(test_file) as im: - # Assert assert im.size == (199, 199) @@ -171,7 +169,6 @@ def test_save_id_section(tmp_path): test_file = "Tests/images/tga_id_field.tga" with Image.open(test_file) as im: - # Save with no id section im.save(out, id_section="") with Image.open(out) as test_im: diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 4f3c8e390..70142747c 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -25,7 +25,6 @@ except ImportError: class TestFileTiff: def test_sanity(self, tmp_path): - filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename) @@ -157,7 +156,6 @@ class TestFileTiff: def test_xyres_tiff(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -171,7 +169,6 @@ class TestFileTiff: def test_xyres_fallback_tiff(self): filename = "Tests/images/compression.tif" with Image.open(filename) as im: - # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -186,7 +183,6 @@ class TestFileTiff: def test_int_resolution(self): filename = "Tests/images/pil168.tif" with Image.open(filename) as im: - # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -381,7 +377,6 @@ class TestFileTiff: def test___str__(self): filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # Act ret = str(im.ifd) @@ -392,7 +387,6 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: - # v2 interface v2_tags = { 256: 55, @@ -630,7 +624,6 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, **kwargs) with Image.open(filename) as im: - # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1061f7d05..a4481d85f 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -54,7 +54,6 @@ def test_rt_metadata(tmp_path): img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -74,14 +73,12 @@ def test_rt_metadata(tmp_path): info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: - assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata(): with Image.open("Tests/images/hopper_g4.tif") as img: - assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4f513d82b..037479f9f 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -18,10 +18,8 @@ except ImportError: def test_read_exif_metadata(): - file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data @@ -64,10 +62,8 @@ def test_write_exif_metadata(): def test_read_icc_profile(): - file_path = "Tests/images/flower2.webp" with Image.open(file_path) as image: - assert image.format == "WEBP" assert image.info.get("icc_profile", None) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 439cb15bc..7c8b54fd1 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -6,7 +6,6 @@ from .helper import assert_image_similar_tofile, hopper def test_load_raw(): - # Test basic EMF open and rendering with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): diff --git a/Tests/test_file_xbm.py b/Tests/test_file_xbm.py index 9c54c6755..d2c05b78a 100644 --- a/Tests/test_file_xbm.py +++ b/Tests/test_file_xbm.py @@ -44,7 +44,6 @@ def test_open(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) @@ -57,7 +56,6 @@ def test_open_filename_with_underscore(): # Act with Image.open(filename) as im: - # Assert assert im.mode == "1" assert im.size == (128, 128) diff --git a/Tests/test_file_xvthumb.py b/Tests/test_file_xvthumb.py index ae53d2b63..9efe7ec14 100644 --- a/Tests/test_file_xvthumb.py +++ b/Tests/test_file_xvthumb.py @@ -10,7 +10,6 @@ TEST_FILE = "Tests/images/hopper.p7" def test_open(): # Act with Image.open(TEST_FILE) as im: - # Assert assert im.format == "XVThumb" diff --git a/Tests/test_image.py b/Tests/test_image.py index ad3346b5a..85e3ff55b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -69,7 +69,6 @@ class TestImage: assert issubclass(UnidentifiedImageError, OSError) def test_sanity(self): - im = Image.new("L", (100, 100)) assert repr(im)[:45] == ">> im.save('Tests/images/hopper_45.png') with Image.open("Tests/images/hopper_45.png") as target: - for (resample, epsilon) in ( + for resample, epsilon in ( (Image.Resampling.NEAREST, 10), (Image.Resampling.BILINEAR, 5), (Image.Resampling.BICUBIC, 0), diff --git a/Tests/test_image_tobitmap.py b/Tests/test_image_tobitmap.py index 178cfcef3..a12ce329f 100644 --- a/Tests/test_image_tobitmap.py +++ b/Tests/test_image_tobitmap.py @@ -4,7 +4,6 @@ from .helper import assert_image_equal, fromstring, hopper def test_sanity(): - with pytest.raises(ValueError): hopper().tobitmap() diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index b839a7b14..d0fea3854 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -50,7 +50,6 @@ def test_add(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2) @@ -63,7 +62,6 @@ def test_add_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add(im1, im2, scale=2.5, offset=100) @@ -87,7 +85,6 @@ def test_add_modulo(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.add_modulo(im1, im2) @@ -111,7 +108,6 @@ def test_blend(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.blend(im1, im2, 0.5) @@ -137,7 +133,6 @@ def test_darker_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -149,7 +144,6 @@ def test_darker_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.darker(im1, im2) @@ -161,7 +155,6 @@ def test_difference(): # Arrange with Image.open("Tests/images/imagedraw_arc_end_le_start.png") as im1: with Image.open("Tests/images/imagedraw_arc_no_loops.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -173,7 +166,6 @@ def test_difference_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_polygon_kite_RGB.png") as im2: - # Act new = ImageChops.difference(im1, im2) @@ -195,7 +187,6 @@ def test_duplicate(): def test_invert(): # Arrange with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im: - # Act new = ImageChops.invert(im) @@ -209,7 +200,6 @@ def test_lighter_image(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -221,7 +211,6 @@ def test_lighter_pixel(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.lighter(im1, im2) @@ -275,7 +264,6 @@ def test_offset(): xoffset = 45 yoffset = 20 with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im: - # Act new = ImageChops.offset(im, xoffset, yoffset) @@ -292,7 +280,6 @@ def test_screen(): # Arrange with Image.open("Tests/images/imagedraw_ellipse_RGB.png") as im1: with Image.open("Tests/images/imagedraw_floodfill_RGB.png") as im2: - # Act new = ImageChops.screen(im1, im2) @@ -305,7 +292,6 @@ def test_subtract(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -319,7 +305,6 @@ def test_subtract_scale_offset(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2, scale=2.5, offset=100) @@ -332,7 +317,6 @@ def test_subtract_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract(im1, im2) @@ -344,7 +328,6 @@ def test_subtract_modulo(): # Arrange with Image.open("Tests/images/imagedraw_chord_RGB.png") as im1: with Image.open("Tests/images/imagedraw_outline_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -358,7 +341,6 @@ def test_subtract_modulo_no_clip(): # Arrange im1 = hopper() with Image.open("Tests/images/imagedraw_chord_RGB.png") as im2: - # Act new = ImageChops.subtract_modulo(im1, im2) @@ -370,7 +352,6 @@ def test_soft_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.soft_light(im1, im2) @@ -383,7 +364,6 @@ def test_hard_light(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.hard_light(im1, im2) @@ -396,7 +376,6 @@ def test_overlay(): # Arrange with Image.open("Tests/images/hopper.png") as im1: with Image.open("Tests/images/hopper-XYZ.png") as im2: - # Act new = ImageChops.overlay(im1, im2) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4c4c41b7b..d4723c924 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -52,7 +52,6 @@ def test_sanity(): def test_valueerror(): with Image.open("Tests/images/chi.gif") as im: - draw = ImageDraw.Draw(im) draw.line((0, 0), fill=(0, 0, 0)) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fc0fbfb9b..412bc10d9 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -30,7 +30,6 @@ SAFEBLOCK = ImageFile.SAFEBLOCK class TestImageFile: def test_parser(self): def roundtrip(format): - im = hopper("L").resize((1000, 1000), Image.Resampling.NEAREST) if format in ("MSP", "XBM"): im = im.convert("1") diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index c9b2fd865..d390f3c1e 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -21,7 +21,6 @@ deformer = Deformer() def test_sanity(): - ImageOps.autocontrast(hopper("L")) ImageOps.autocontrast(hopper("RGB")) @@ -419,7 +418,6 @@ def test_autocontrast_cutoff(): def test_autocontrast_mask_toy_input(): # Test the mask argument of autocontrast with Image.open("Tests/images/bw_gradient.png") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0 = img.size[0] // 4 @@ -439,7 +437,6 @@ def test_autocontrast_mask_toy_input(): def test_autocontrast_mask_real_input(): # Test the autocontrast with a rectangular mask with Image.open("Tests/images/iptc.jpg") as img: - rect_mask = Image.new("L", img.size, 0) draw = ImageDraw.Draw(rect_mask) x0, y0 = img.size[0] // 2, img.size[1] // 2 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 5bda28117..ac99ef381 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, assert_image_equal_tofile def test_sanity(): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 @@ -23,7 +22,6 @@ def test_reload(): def test_getcolor(): - palette = ImagePalette.ImagePalette() assert len(palette.palette) == 0 assert len(palette.colors) == 0 @@ -84,7 +82,6 @@ def test_getcolor_not_special(index, palette): def test_file(tmp_path): - palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) f = str(tmp_path / "temp.lut") diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..8f8a9f449 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -8,7 +8,6 @@ from PIL import Image, ImagePath def test_path(): - p = ImagePath.Path(list(range(10))) # sequence interface diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 6af7e7602..62f528332 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -6,7 +6,6 @@ from .helper import assert_image_equal, hopper, skip_unless_feature def test_sanity(tmp_path): - test_file = str(tmp_path / "temp.im") im = hopper("RGB") diff --git a/Tests/test_imagestat.py b/Tests/test_imagestat.py index 5717fe150..b3b5db13f 100644 --- a/Tests/test_imagestat.py +++ b/Tests/test_imagestat.py @@ -6,7 +6,6 @@ from .helper import hopper def test_sanity(): - im = hopper() st = ImageStat.Stat(im) @@ -31,7 +30,6 @@ def test_sanity(): def test_hopper(): - im = hopper() st = ImageStat.Stat(im) @@ -45,7 +43,6 @@ def test_hopper(): def test_constant(): - im = Image.new("L", (128, 128), 128) st = ImageStat.Stat(im) diff --git a/Tests/test_lib_image.py b/Tests/test_lib_image.py index 37ed3659d..f6818be46 100644 --- a/Tests/test_lib_image.py +++ b/Tests/test_lib_image.py @@ -4,7 +4,6 @@ from PIL import Image def test_setmode(): - im = Image.new("L", (1, 1), 255) im.im.setmode("1") assert im.im.getpixel((0, 0)) == 255 diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index efcdab9ec..dcdee3d41 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -42,7 +42,6 @@ def test_basic(tmp_path, mode): im_in.save(filename) with Image.open(filename) as im_out: - verify(im_in) verify(im_out) @@ -87,7 +86,6 @@ def test_tobytes(): def test_convert(): - im = original.copy() verify(im.convert("I;16")) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 3de7ec30f..a8bbcbdb8 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -235,7 +235,6 @@ def test_no_resource_warning_for_numpy_array(): test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: - # Act/Assert with warnings.catch_warnings(): array(im) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 23eb9e39f..2f6d05888 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -89,7 +89,6 @@ def test_pickle_la_mode_with_palette(tmp_path): def test_pickle_tell(): # Arrange with Image.open("Tests/images/hopper.webp") as image: - # Act: roundtrip unpickled_image = pickle.loads(pickle.dumps(image)) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 12f475df0..6e3fcec90 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -7,7 +7,6 @@ from .helper import hopper def _test_equal(num, denom, target): - t = IFDRational(num, denom) assert target == t @@ -15,7 +14,6 @@ def _test_equal(num, denom, target): def test_sanity(): - _test_equal(1, 1, 1) _test_equal(1, 1, Fraction(1, 1)) diff --git a/Tests/test_webp_leaks.py b/Tests/test_webp_leaks.py index 34197c14f..5bd9bacdb 100644 --- a/Tests/test_webp_leaks.py +++ b/Tests/test_webp_leaks.py @@ -9,7 +9,6 @@ test_file = "Tests/images/hopper.webp" @skip_unless_feature("webp") class TestWebPLeaks(PillowLeakTestCase): - mem_limit = 3 * 1024 # kb iterations = 100 diff --git a/setup.py b/setup.py index 4382c1a97..8f7f223f8 100755 --- a/setup.py +++ b/setup.py @@ -430,7 +430,6 @@ class pil_build_ext(build_ext): return sdk_path def build_extensions(self): - library_dirs = [] include_dirs = [] @@ -917,7 +916,6 @@ class pil_build_ext(build_ext): self.summary_report(feature) def summary_report(self, feature): - print("-" * 68) print("PIL SETUP SUMMARY") print("-" * 68) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index e13b18f27..5bda0a5b0 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -223,7 +223,6 @@ class BmpImageFile(ImageFile.ImageFile): # --------------- Once the header is processed, process the palette/LUT if self.mode == "P": # Paletted for 1, 4 and 8 bit images - # ---------------------------------------------------- 1-bit images if not (0 < file_info["colors"] <= 65536): msg = f"Unsupported BMP Palette size ({file_info['colors']})" @@ -360,7 +359,6 @@ class BmpRleDecoder(ImageFile.PyDecoder): # Image plugin for the DIB format (BMP alias) # ============================================================================= class DibImageFile(BmpImageFile): - format = "DIB" format_description = "Windows Bitmap" diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index a0da1b786..0425bbd75 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class BufrStubImageFile(ImageFile.StubImageFile): - format = "BUFR" format_description = "BUFR" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index aedc6ce7f..94efff341 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -32,12 +32,10 @@ def _accept(prefix): class CurImageFile(BmpImagePlugin.BmpImageFile): - format = "CUR" format_description = "Windows Cursor" def _open(self): - offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index 81c0314f0..cde9d42f0 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -37,13 +37,11 @@ def _accept(prefix): class DcxImageFile(PcxImageFile): - format = "DCX" format_description = "Intel DCX" _close_exclusive_fp_after_loading = False def _open(self): - # Header s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd68c13e5..60cb46df9 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -286,7 +286,6 @@ class EpsImageFile(ImageFile.ImageFile): # Scan for an "ImageData" descriptor while s[:1] == "%": - if len(s) > 255: msg = "not an EPS file" raise SyntaxError(msg) @@ -317,7 +316,6 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) def _find_offset(self, fp): - s = fp.read(4) if s == b"%!PS": diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 536bc1fe6..1185ef2d3 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -19,7 +19,6 @@ def _accept(prefix): class FitsImageFile(ImageFile.ImageFile): - format = "FITS" format_description = "FITS" diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py index 86eb2d5a2..50948ec42 100644 --- a/src/PIL/FitsStubImagePlugin.py +++ b/src/PIL/FitsStubImagePlugin.py @@ -44,7 +44,6 @@ def register_handler(handler): class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format format_description = FitsImagePlugin.FitsImageFile.format_description diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 66681939d..f4e89a03e 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -40,13 +40,11 @@ def _accept(prefix): class FliImageFile(ImageFile.ImageFile): - format = "FLI" format_description = "Autodesk FLI/FLC Animation" _close_exclusive_fp_after_loading = False def _open(self): - # HEAD s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): diff --git a/src/PIL/FontFile.py b/src/PIL/FontFile.py index c5fc80b37..5ec0a6632 100644 --- a/src/PIL/FontFile.py +++ b/src/PIL/FontFile.py @@ -36,7 +36,6 @@ class FontFile: bitmap = None def __init__(self): - self.info = {} self.glyph = [None] * 256 diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8ddc6b40b..d145d01f7 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -48,7 +48,6 @@ def _accept(prefix): class FpxImageFile(ImageFile.ImageFile): - format = "FPX" format_description = "FlashPix" @@ -157,7 +156,6 @@ class FpxImageFile(ImageFile.ImageFile): self.tile = [] for i in range(0, len(s), length): - x1 = min(xsize, x + xtile) y1 = min(ysize, y + ytile) @@ -174,7 +172,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 1: - # FIXME: the fill decoder is not implemented self.tile.append( ( @@ -186,7 +183,6 @@ class FpxImageFile(ImageFile.ImageFile): ) elif compression == 2: - internal_color_conversion = s[14] jpeg_tables = s[15] rawmode = self.rawmode @@ -234,7 +230,6 @@ class FpxImageFile(ImageFile.ImageFile): self.fp = None def load(self): - if not self.fp: self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"]) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 828a45ced..994a6e8eb 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -37,7 +37,6 @@ def _accept(prefix): class GbrImageFile(ImageFile.ImageFile): - format = "GBR" format_description = "GIMP brush file" diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 3875dc866..7dda4f143 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -44,7 +44,6 @@ class GdImageFile(ImageFile.ImageFile): format_description = "GD uncompressed images" def _open(self): - # Header s = self.fp.read(1037) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 6ee1bd3d8..eadee1560 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -61,7 +61,6 @@ def _accept(prefix): class GifImageFile(ImageFile.ImageFile): - format = "GIF" format_description = "Compuserve GIF" _close_exclusive_fp_after_loading = False @@ -81,7 +80,6 @@ class GifImageFile(ImageFile.ImageFile): return False def _open(self): - # Screen s = self.fp.read(13) if not _accept(s): @@ -157,7 +155,6 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame, update_image=True): - if frame == 0: # rewind self.__offset = 0 @@ -195,7 +192,6 @@ class GifImageFile(ImageFile.ImageFile): interlace = None frame_dispose_extent = None while True: - if not s: s = self.fp.read(1) if not s or s == b";": @@ -579,7 +575,6 @@ def _getbbox(base_im, im_frame): def _write_multiple_frames(im, fp, palette): - duration = im.encoderinfo.get("duration") disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) @@ -752,7 +747,6 @@ def _write_local_header(fp, im, offset, flags): def _save_netpbm(im, fp, filename): - # Unused by default. # To use, uncomment the register_save call at the end of the file. # diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index b5c5e3ca4..8e801be0b 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -64,18 +64,15 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing] class GradientFile: - gradient = None def getpalette(self, entries=256): - palette = [] ix = 0 x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix] for i in range(entries): - x = i / (entries - 1) while x1 < x: @@ -105,7 +102,6 @@ class GimpGradientFile(GradientFile): """File handler for GIMP's gradient format.""" def __init__(self, fp): - if fp.readline()[:13] != b"GIMP Gradient": msg = "not a GIMP gradient file" raise SyntaxError(msg) @@ -121,7 +117,6 @@ class GimpGradientFile(GradientFile): gradient = [] for i in range(count): - s = fp.readline().split() w = [float(x) for x in s[:11]] diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 2e9cbe58d..d38892894 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -25,7 +25,6 @@ class GimpPaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [o8(i) * 3 for i in range(256)] if fp.readline()[:12] != b"GIMP Palette": @@ -33,7 +32,6 @@ class GimpPaletteFile: raise SyntaxError(msg) for i in range(256): - s = fp.readline() if not s: break @@ -55,5 +53,4 @@ class GimpPaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2088eb7b0..8a799f19c 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class GribStubImageFile(ImageFile.StubImageFile): - format = "GRIB" format_description = "GRIB" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index d6f283739..bba05ed65 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class HDF5StubImageFile(ImageFile.StubImageFile): - format = "HDF5" format_description = "HDF5" def _open(self): - offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index e76d0c35a..c2f050edd 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -135,7 +135,6 @@ def read_png_or_jpeg2000(fobj, start_length, size): class IcnsFile: - SIZES = { (512, 512, 2): [(b"ic10", read_png_or_jpeg2000)], (512, 512, 1): [(b"ic09", read_png_or_jpeg2000)], @@ -189,7 +188,7 @@ class IcnsFile: def itersizes(self): sizes = [] for size, fmts in self.SIZES.items(): - for (fmt, reader) in fmts: + for fmt, reader in fmts: if fmt in self.dct: sizes.append(size) break diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 568e6d38d..a188f8fdc 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -185,7 +185,7 @@ class IcoFile: return {(h["width"], h["height"]) for h in self.entry} def getentryindex(self, size, bpp=False): - for (i, h) in enumerate(self.entry): + for i, h in enumerate(self.entry): if size == h["dim"] and (bpp is False or bpp == h["color_depth"]): return i return 0 diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 875a20326..746743f65 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -115,13 +115,11 @@ def number(s): class ImImageFile(ImageFile.ImageFile): - format = "IM" format_description = "IFUNC Image Memory" _close_exclusive_fp_after_loading = False def _open(self): - # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. @@ -140,7 +138,6 @@ class ImImageFile(ImageFile.ImageFile): self.rawmode = "L" while True: - s = self.fp.read(1) # Some versions of IFUNC uses \n\r instead of \r\n... @@ -169,7 +166,6 @@ class ImImageFile(ImageFile.ImageFile): raise SyntaxError(msg) from e if m: - k, v = m.group(1, 2) # Don't know if this is the correct encoding, @@ -200,7 +196,6 @@ class ImImageFile(ImageFile.ImageFile): n += 1 else: - msg = "Syntax error in IM header: " + s.decode("ascii", "replace") raise SyntaxError(msg) @@ -252,7 +247,6 @@ class ImImageFile(ImageFile.ImageFile): self._fp = self.fp # FIXME: hack if self.rawmode[:2] == "F;": - # ifunc95 formats try: # use bit decoder (if necessary) @@ -332,7 +326,6 @@ SAVE = { def _save(im, fp, filename): - try: image_type, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ad0d25add..81123d070 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -153,6 +153,7 @@ def isImageType(t): # # Constants + # transpose class Transpose(IntEnum): FLIP_LEFT_RIGHT = 0 @@ -391,7 +392,6 @@ def init(): def _getdecoder(mode, decoder_name, args, extra=()): - # tweak arguments if args is None: args = () @@ -415,7 +415,6 @@ def _getdecoder(mode, decoder_name, args, extra=()): def _getencoder(mode, encoder_name, args, extra=()): - # tweak arguments if args is None: args = () diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ce29a163b..163828d31 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -928,8 +928,8 @@ def floodfill(image, xy, value, border=None, thresh=0): full_edge = set() while edge: new_edge = set() - for (x, y) in edge: # 4 adjacent method - for (s, t) in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + for x, y in edge: # 4 adjacent method + for s, t in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): # If already processed, or if a coordinate is negative, skip if (s, t) in full_edge or s < 0 or t < 0: continue diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 12391955f..132490a8e 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -395,7 +395,6 @@ class Parser: # parse what we have if self.decoder: - if self.offset > 0: # skip header skip = min(len(self.data), self.offset) @@ -420,14 +419,12 @@ class Parser: self.data = self.data[n:] elif self.image: - # if we end up here with no decoder, this file cannot # be incrementally parsed. wait until we've gotten all # available data pass else: - # attempt to open this file try: with io.BytesIO(self.data) as fp: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b144c3dd2..bd13c391e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -90,7 +90,6 @@ class ImageFont: """PIL font wrapper""" def _load_pilfont(self, filename): - with open(filename, "rb") as fp: image = None for ext in (".png", ".gif", ".pbm"): @@ -116,7 +115,6 @@ class ImageFont: image.close() def _load_pilfont_data(self, file, image): - # read PILfont header if file.readline() != b"PILfont\n": msg = "Not a PILfont file" diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 16c83f4e4..301c593c7 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -205,7 +205,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (2-color) if mid is None: - range_map = range(0, whitepoint - blackpoint) for i in range_map: @@ -215,7 +214,6 @@ def colorize(image, black, white, mid=None, blackpoint=0, whitepoint=255, midpoi # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) range_map2 = range(0, whitepoint - midpoint) diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index fe0d32155..e455c0459 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -248,11 +248,9 @@ def wedge(mode="RGB"): def load(filename): - # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 29d900bef..f0e73fb90 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -390,7 +390,6 @@ else: if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 ImageShow.py imagefile [title]") sys.exit() diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index 09a6356fa..ef569ed2e 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -97,7 +97,6 @@ class PhotoImage: """ def __init__(self, image=None, size=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) @@ -209,7 +208,6 @@ class BitmapImage: """ def __init__(self, image=None, **kw): - # Tk compatibility: file or data if image is None: image = _get_image_from_kw(kw) diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index cfeadd53c..ac267457b 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -30,12 +30,10 @@ field = re.compile(rb"([a-z]*) ([^ \r\n]*)") class ImtImageFile(ImageFile.ImageFile): - format = "IMT" format_description = "IM Tools" def _open(self): - # Quick rejection: if there's not a LF among the first # 100 bytes, this is (probably) not a text header. @@ -47,7 +45,6 @@ class ImtImageFile(ImageFile.ImageFile): xsize = ysize = 0 while True: - if buffer: s = buffer[:1] buffer = buffer[1:] @@ -57,7 +54,6 @@ class ImtImageFile(ImageFile.ImageFile): break if s == b"\x0C": - # image data begins self.tile = [ ( @@ -71,7 +67,6 @@ class ImtImageFile(ImageFile.ImageFile): break else: - # read key/value pair if b"\n" not in buffer: buffer += self.fp.read(100) diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 774817569..4c47b55c1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -48,7 +48,6 @@ def dump(c): class IptcImageFile(ImageFile.ImageFile): - format = "IPTC" format_description = "IPTC/NAA" @@ -84,7 +83,6 @@ class IptcImageFile(ImageFile.ImageFile): return tag, size def _open(self): - # load descriptive fields while True: offset = self.fp.tell() @@ -134,7 +132,6 @@ class IptcImageFile(ImageFile.ImageFile): ] def load(self): - if len(self.tile) != 1 or self.tile[0][0] != "iptc": return ImageFile.ImageFile.load(self) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index b9c80236e..d7ddbe0d9 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -344,12 +344,10 @@ def _accept(prefix): class JpegImageFile(ImageFile.ImageFile): - format = "JPEG" format_description = "JPEG (ISO 10918)" def _open(self): - s = self.fp.read(3) if not _accept(s): @@ -370,7 +368,6 @@ class JpegImageFile(ImageFile.ImageFile): self.icclist = [] while True: - i = s[0] if i == 0xFF: s = s + self.fp.read(1) @@ -418,7 +415,6 @@ class JpegImageFile(ImageFile.ImageFile): return s def draft(self, mode, size): - if len(self.tile) != 1: return @@ -455,7 +451,6 @@ class JpegImageFile(ImageFile.ImageFile): return self.mode, box def load_djpeg(self): - # ALTERNATIVE: handle JPEGs via the IJG command line utilities f, path = tempfile.mkstemp() diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 8d4d826aa..17c008b9a 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -30,12 +30,10 @@ def _accept(s): class McIdasImageFile(ImageFile.ImageFile): - format = "MCIDAS" format_description = "McIdas area file" def _open(self): - # parse area file directory s = self.fp.read(256) if not _accept(s) or len(s) != 256: diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index e7e1054a3..8dd9f2909 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -34,13 +34,11 @@ def _accept(prefix): class MicImageFile(TiffImagePlugin.TiffImageFile): - format = "MIC" format_description = "Microsoft Image Composer" _close_exclusive_fp_after_loading = False def _open(self): - # read the OLE directory and see if this is a likely # to be a Microsoft Image Composer file diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index 2d799d6d8..d96d3a11c 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -58,12 +58,10 @@ class BitStream: class MpegImageFile(ImageFile.ImageFile): - format = "MPEG" format_description = "MPEG" def _open(self): - s = BitStream(self.fp) if s.read(32) != 0x1B3: diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index b1ec2c7bc..f9261c77d 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -101,7 +101,6 @@ def _save_all(im, fp, filename): class MpoImageFile(JpegImagePlugin.JpegImageFile): - format = "MPO" format_description = "MPO (CIPA DC-007)" _close_exclusive_fp_after_loading = False diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 5420894dc..c6567b2ae 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class MspImageFile(ImageFile.ImageFile): - format = "MSP" format_description = "Windows Paint" def _open(self): - # Header s = self.fp.read(32) if not _accept(s): @@ -111,7 +109,6 @@ class MspDecoder(ImageFile.PyDecoder): _pulls_fd = True def decode(self, buffer): - img = io.BytesIO() blank_line = bytearray((0xFF,) * ((self.state.xsize + 7) // 8)) try: @@ -162,7 +159,6 @@ Image.register_decoder("MSP", MspDecoder) def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as MSP" raise OSError(msg) diff --git a/src/PIL/PaletteFile.py b/src/PIL/PaletteFile.py index 07acd5580..4a2c497fc 100644 --- a/src/PIL/PaletteFile.py +++ b/src/PIL/PaletteFile.py @@ -22,11 +22,9 @@ class PaletteFile: rawmode = "RGB" def __init__(self, fp): - self.palette = [(i, i, i) for i in range(256)] while True: - s = fp.readline() if not s: @@ -50,5 +48,4 @@ class PaletteFile: self.palette = b"".join(self.palette) def getpalette(self): - return self.palette, self.rawmode diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 109aad9ab..a88a90791 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -112,9 +112,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00} def _save(im, fp, filename): - if im.mode == "P": - # we assume this is a color Palm image with the standard colormap, # unless the "info" dict has a "custom-colormap" field @@ -147,14 +145,12 @@ def _save(im, fp, filename): version = 1 elif im.mode == "1": - # monochrome -- write it inverted, as is the Palm standard rawmode = "1;I" bpp = 1 version = 0 else: - msg = f"cannot write mode {im.mode} as Palm" raise OSError(msg) diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index 5802d386a..e390f3fe5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -24,12 +24,10 @@ from . import Image, ImageFile class PcdImageFile(ImageFile.ImageFile): - format = "PCD" format_description = "Kodak PhotoCD" def _open(self): - # rough self.fp.seek(2048) s = self.fp.read(2048) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index ecce1b097..d5f510f03 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -58,7 +58,6 @@ class PcfFontFile(FontFile.FontFile): name = "name" def __init__(self, fp, charset_encoding="iso8859-1"): - self.charset_encoding = charset_encoding magic = l32(fp.read(4)) @@ -92,7 +91,6 @@ class PcfFontFile(FontFile.FontFile): self.glyph[ch] = glyph def _getformat(self, tag): - format, size, offset = self.toc[tag] fp = self.fp @@ -108,7 +106,6 @@ class PcfFontFile(FontFile.FontFile): return fp, format, i16, i32 def _load_properties(self): - # # font properties @@ -136,7 +133,6 @@ class PcfFontFile(FontFile.FontFile): return properties def _load_metrics(self): - # # font metrics @@ -147,7 +143,6 @@ class PcfFontFile(FontFile.FontFile): append = metrics.append if (format & 0xFF00) == 0x100: - # "compressed" metrics for i in range(i16(fp.read(2))): left = i8(fp.read(1)) - 128 @@ -160,7 +155,6 @@ class PcfFontFile(FontFile.FontFile): append((xsize, ysize, left, right, width, ascent, descent, 0)) else: - # "jumbo" metrics for i in range(i32(fp.read(4))): left = i16(fp.read(2)) @@ -176,7 +170,6 @@ class PcfFontFile(FontFile.FontFile): return metrics def _load_bitmaps(self, metrics): - # # bitmap data diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index 3202475dc..f42c2456b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -45,12 +45,10 @@ def _accept(prefix): class PcxImageFile(ImageFile.ImageFile): - format = "PCX" format_description = "Paintbrush" def _open(self): - # header s = self.fp.read(128) if not _accept(s): @@ -143,7 +141,6 @@ SAVE = { def _save(im, fp, filename): - try: version, bits, planes, rawmode = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 8d0a34dba..7eb82228a 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -35,12 +35,10 @@ def _accept(prefix): class PixarImageFile(ImageFile.ImageFile): - format = "PIXAR" format_description = "PIXAR raster image" def _open(self): - # assuming a 4-byte magic label s = self.fp.read(4) if not _accept(s): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index b6626bbc5..9078957dc 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -161,7 +161,6 @@ def _crc32(data, seed=0): class ChunkStream: def __init__(self, fp): - self.fp = fp self.queue = [] @@ -195,7 +194,6 @@ class ChunkStream: self.queue = self.fp = None def push(self, cid, pos, length): - self.queue.append((cid, pos, length)) def call(self, cid, pos, length): @@ -230,7 +228,6 @@ class ChunkStream: self.fp.read(4) def verify(self, endchunk=b"IEND"): - # Simple approach; just calculate checksum for all remaining # blocks. Must be called directly after open. @@ -397,7 +394,6 @@ class PngStream(ChunkStream): self._seq_num = self.rewind_state["seq_num"] def chunk_iCCP(self, pos, length): - # ICC profile s = ImageFile._safe_read(self.fp, length) # according to PNG spec, the iCCP chunk contains: @@ -425,7 +421,6 @@ class PngStream(ChunkStream): return s def chunk_IHDR(self, pos, length): - # image header s = ImageFile._safe_read(self.fp, length) if length < 13: @@ -446,7 +441,6 @@ class PngStream(ChunkStream): return s def chunk_IDAT(self, pos, length): - # image data if "bbox" in self.im_info: tile = [("zip", self.im_info["bbox"], pos, self.im_rawmode)] @@ -459,12 +453,10 @@ class PngStream(ChunkStream): raise EOFError def chunk_IEND(self, pos, length): - # end of PNG image raise EOFError def chunk_PLTE(self, pos, length): - # palette s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -472,7 +464,6 @@ class PngStream(ChunkStream): return s def chunk_tRNS(self, pos, length): - # transparency s = ImageFile._safe_read(self.fp, length) if self.im_mode == "P": @@ -524,7 +515,6 @@ class PngStream(ChunkStream): return s def chunk_pHYs(self, pos, length): - # pixels per unit s = ImageFile._safe_read(self.fp, length) if length < 9: @@ -542,7 +532,6 @@ class PngStream(ChunkStream): return s def chunk_tEXt(self, pos, length): - # text s = ImageFile._safe_read(self.fp, length) try: @@ -562,7 +551,6 @@ class PngStream(ChunkStream): return s def chunk_zTXt(self, pos, length): - # compressed text s = ImageFile._safe_read(self.fp, length) try: @@ -597,7 +585,6 @@ class PngStream(ChunkStream): return s def chunk_iTXt(self, pos, length): - # international text r = s = ImageFile._safe_read(self.fp, length) try: @@ -721,12 +708,10 @@ def _accept(prefix): class PngImageFile(ImageFile.ImageFile): - format = "PNG" format_description = "Portable network graphics" def _open(self): - if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -740,7 +725,6 @@ class PngImageFile(ImageFile.ImageFile): self.png = PngStream(self.fp) while True: - # # get next chunk @@ -1264,7 +1248,6 @@ def _save(im, fp, filename, chunk=putchunk, save_all=False): mode = im.mode if mode == "P": - # # attempt to minimize storage requirements for palette images if "bits" in im.encoderinfo: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index dee2f1e15..5aa418044 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -51,7 +51,6 @@ def _accept(prefix): class PpmImageFile(ImageFile.ImageFile): - format = "PPM" format_description = "Pbmplus image" diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 7e8d12759..5a5d60d56 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -51,13 +51,11 @@ def _accept(prefix): class PsdImageFile(ImageFile.ImageFile): - format = "PSD" format_description = "Adobe Photoshop" _close_exclusive_fp_after_loading = False def _open(self): - read = self.fp.read # @@ -177,7 +175,6 @@ def _layerinfo(fp, ct_bytes): raise SyntaxError(msg) for _ in range(abs(ct)): - # bounding box y0 = i32(read(4)) x0 = i32(read(4)) @@ -250,7 +247,6 @@ def _layerinfo(fp, ct_bytes): def _maketile(file, mode, bbox, channels): - tile = None read = file.read diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index d533c55e5..3662ffd15 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -49,12 +49,10 @@ MODES = { ## # Image plugin for SGI images. class SgiImageFile(ImageFile.ImageFile): - format = "SGI" format_description = "SGI Image File Format" def _open(self): - # HEAD headlen = 512 s = self.fp.read(headlen) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 1192c2d73..eac27e679 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -91,7 +91,6 @@ def isSpiderImage(filename): class SpiderImageFile(ImageFile.ImageFile): - format = "SPIDER" format_description = "Spider 2D image" _close_exclusive_fp_after_loading = False @@ -200,6 +199,7 @@ class SpiderImageFile(ImageFile.ImageFile): # -------------------------------------------------------------------- # Image series + # given a list of filenames, return a list of images def loadImageSeries(filelist=None): """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" @@ -289,7 +289,6 @@ Image.register_open(SpiderImageFile.format, SpiderImageFile) Image.register_save(SpiderImageFile.format, _save_spider) if __name__ == "__main__": - if len(sys.argv) < 2: print("Syntax: python3 SpiderImagePlugin.py [infile] [outfile]") sys.exit() diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index c64de4444..6712583d7 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -30,12 +30,10 @@ def _accept(prefix): class SunImageFile(ImageFile.ImageFile): - format = "SUN" format_description = "Sun Raster File" def _open(self): - # The Sun Raster file header is 32 bytes in length # and has the following format: diff --git a/src/PIL/TarIO.py b/src/PIL/TarIO.py index 20e8a083f..32928f6af 100644 --- a/src/PIL/TarIO.py +++ b/src/PIL/TarIO.py @@ -32,7 +32,6 @@ class TarIO(ContainerIO.ContainerIO): self.fh = open(tarfile, "rb") while True: - s = self.fh.read(512) if len(s) != 512: msg = "unexpected end of tar file" diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 53fe6ef5c..67dfc3d3c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -46,12 +46,10 @@ MODES = { class TgaImageFile(ImageFile.ImageFile): - format = "TGA" format_description = "Targa" def _open(self): - # process header s = self.fp.read(18) @@ -174,7 +172,6 @@ SAVE = { def _save(im, fp, filename): - try: rawmode, bits, colormaptype, imagetype = SAVE[im.mode] except KeyError as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index baa9abad8..2cf5b173f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -793,7 +793,6 @@ class ImageFileDirectory_v2(MutableMapping): return ret def load(self, fp): - self.reset() self._offset = fp.tell() @@ -938,7 +937,6 @@ class ImageFileDirectory_v2(MutableMapping): return result def save(self, fp): - if fp.tell() == 0: # skip TIFF header on subsequent pages # tiff header -- PIL always starts the first IFD at offset 8 fp.write(self._prefix + self._pack("HL", 42, 8)) @@ -1059,7 +1057,6 @@ ImageFileDirectory = ImageFileDirectory_v1 class TiffImageFile(ImageFile.ImageFile): - format = "TIFF" format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False @@ -1582,7 +1579,6 @@ SAVE_INFO = { def _save(im, fp, filename): - try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 0dc695a88..e4f47aa04 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -28,7 +28,6 @@ from ._binary import i32le as i32 class WalImageFile(ImageFile.ImageFile): - format = "WAL" format_description = "Quake2 Texture" diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 1d074f78c..d060dd4b8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -35,7 +35,6 @@ def _accept(prefix): class WebPImageFile(ImageFile.ImageFile): - format = "WEBP" format_description = "WebP image" __loaded = 0 diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 639730b8e..0ecab56a8 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -75,7 +75,6 @@ def _accept(prefix): class WmfStubImageFile(ImageFile.StubImageFile): - format = "WMF" format_description = "Windows Metafile" @@ -86,7 +85,6 @@ class WmfStubImageFile(ImageFile.StubImageFile): s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": - # placeable windows metafile # get units per inch diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index f0e05e867..aa4a01f4e 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -41,12 +41,10 @@ def _accept(prefix): class XVThumbImageFile(ImageFile.ImageFile): - format = "XVThumb" format_description = "XV thumbnail image" def _open(self): - # check magic if not _accept(self.fp.read(6)): msg = "not an XV thumbnail file" diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index ad18e0031..3c12564c9 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -44,12 +44,10 @@ def _accept(prefix): class XbmImageFile(ImageFile.ImageFile): - format = "XBM" format_description = "X11 Bitmap" def _open(self): - m = xbm_head.match(self.fp.read(512)) if not m: @@ -69,7 +67,6 @@ class XbmImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "1": msg = f"cannot write mode {im.mode} as XBM" raise OSError(msg) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5fae4cd68..5d5bdc3ed 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -33,12 +33,10 @@ def _accept(prefix): class XpmImageFile(ImageFile.ImageFile): - format = "XPM" format_description = "X11 Pixel Map" def _open(self): - if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) @@ -68,7 +66,6 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() if s[-2:] == b"\r\n": s = s[:-2] @@ -79,9 +76,7 @@ class XpmImageFile(ImageFile.ImageFile): s = s[2:-2].split() for i in range(0, len(s), 2): - if s[i] == b"c": - # process colour key rgb = s[i + 1] if rgb == b"None": @@ -99,7 +94,6 @@ class XpmImageFile(ImageFile.ImageFile): break else: - # missing colour key msg = "cannot read this XPM file" raise ValueError(msg) @@ -110,7 +104,6 @@ class XpmImageFile(ImageFile.ImageFile): self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] def load_read(self, bytes): - # # load all image data in one chunk From e79460e775a69c2cd9896144c3b340c4f98e3ad5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 09:46:01 +1100 Subject: [PATCH 035/155] Removed wget dependency --- .github/workflows/test-mingw.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ef8214649..737da7b94 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -59,8 +59,7 @@ jobs: ${{ matrix.package }}-python3-numpy \ ${{ matrix.package }}-python3-olefile \ ${{ matrix.package }}-python3-pip \ - ${{ matrix.package }}-python3-setuptools \ - ${{ matrix.package }}-wget + ${{ matrix.package }}-python3-setuptools if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then pacman -S --noconfirm \ From e7d2750997cfbf88a6c93cbe1e598c8054d1676a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:01:10 +1100 Subject: [PATCH 036/155] Updated test images origin --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 790404535..1dd6c9175 100644 --- a/.gitignore +++ b/.gitignore @@ -79,7 +79,7 @@ docs/_build/ # JetBrains .idea -# Extra test images installed from pillow-depends/test_images +# Extra test images installed from python-pillow/test-images Tests/images/README.md Tests/images/crash_1.tif Tests/images/crash_2.tif From ed1d6633a1db0bf7e894bd372dcccd647cc0ee85 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 8 Feb 2023 10:53:59 +1100 Subject: [PATCH 037/155] Use checkout action for test-images repository --- .github/workflows/test-windows.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cf160a997..e938e999d 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -38,6 +38,12 @@ jobs: repository: python-pillow/pillow-depends path: winbuild\depends + - name: Checkout extra test images + uses: actions/checkout@v3 + with: + repository: python-pillow/test-images + path: Tests\test-images + # sets env: pythonLocation - name: Set up Python uses: actions/setup-python@v4 @@ -63,9 +69,7 @@ jobs: echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH # Install extra test images - curl -fsSL -o pillow-test-images.zip https://github.com/python-pillow/test-images/archive/main.zip - 7z x pillow-test-images.zip -oc:\ - xcopy /S /Y c:\test-images-main\* Tests\images\ + xcopy /S /Y Tests\test-images\* Tests\images # make cache key depend on VS version & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" ` From 165675314605d7363605918698df091b405ba283 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Tue, 7 Feb 2023 22:48:33 -0800 Subject: [PATCH 038/155] Add docstrings for getixif() and Exif --- src/PIL/Image.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 81123d070..d47c57334 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1432,6 +1432,11 @@ class Image: return {get_name(root.tag): get_value(root)} def getexif(self): + """ + Gets EXIF data of the image. + + :returns: an :py:class:`~PIL.Image.Exif` object. + """ if self._exif is None: self._exif = Exif() self._exif._loaded = False @@ -3601,6 +3606,20 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): + """ + Exif class provides read and write access to EXIF image data. + + Only basic information is available on the root level, in Exif object + itself. In order to access the rest, obtain their respective IFDs using + :py:meth:`~PIL.Image.Exif.get_ifd` method and one of + :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and + `GPSInfo`). + + Both root Exif and child IFD objects support dict interface and can be + indexed by int values that are available as enum members of + :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and + :py:class:`~PIL.ExifTags.Interop`. + """ endian = None bigtiff = False From ed3cd75630352b26ee3b90a05c3d9b7fc0de041f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:11:54 +0200 Subject: [PATCH 039/155] Use 'rmdir' instead of 'rm -r' Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- depends/install_extra_test_images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_extra_test_images.sh b/depends/install_extra_test_images.sh index 941bfbe84..1ef6f4e97 100755 --- a/depends/install_extra_test_images.sh +++ b/depends/install_extra_test_images.sh @@ -9,4 +9,4 @@ mv $archive/* ../Tests/images/ # Cleanup old tarball and empty directory rm $archive.tar.gz -rm -r $archive +rmdir $archive From f679e410bd3ee7be8e4c6dce0df0ca4cb4d1fb47 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Feb 2023 10:12:14 +0200 Subject: [PATCH 040/155] Use 'rmdir' instead of 'rm -r' --- depends/download-and-extract.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/download-and-extract.sh b/depends/download-and-extract.sh index d9608e782..a318bfafd 100755 --- a/depends/download-and-extract.sh +++ b/depends/download-and-extract.sh @@ -8,5 +8,5 @@ if [ ! -f $archive.tar.gz ]; then wget -O $archive.tar.gz $url fi -rm -r $archive +rmdir $archive tar -xvzf $archive.tar.gz From c0a811e11678c3a6d8869fdc3739c7635b83ddd4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 08:36:41 +1100 Subject: [PATCH 041/155] Updated libjpeg-turbo to 2.1.5.1 --- 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 89903c621..8a4494103 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -109,9 +109,9 @@ header = [ deps = { "libjpeg": { "url": SF_PROJECTS - + "/libjpeg-turbo/files/2.1.5/libjpeg-turbo-2.1.5.tar.gz/download", - "filename": "libjpeg-turbo-2.1.5.tar.gz", - "dir": "libjpeg-turbo-2.1.5", + + "/libjpeg-turbo/files/2.1.5.1/libjpeg-turbo-2.1.5.1.tar.gz/download", + "filename": "libjpeg-turbo-2.1.5.1.tar.gz", + "dir": "libjpeg-turbo-2.1.5.1", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 0eac4f1942b278f618761c91ccec3f4422b90300 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Wed, 8 Feb 2023 20:34:45 -0800 Subject: [PATCH 042/155] Fix syntax --- src/PIL/Image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d47c57334..f5fa4ec0d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3612,8 +3612,8 @@ class Exif(MutableMapping): Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably `Exif` and - `GPSInfo`). + :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and + ``GPSInfo``). Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of From 074c6afdc7d6c6990b9738c68c0fa3951d4f0855 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 04:40:57 +0000 Subject: [PATCH 043/155] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/Image.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f5fa4ec0d..813c237ad 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1434,7 +1434,7 @@ class Image: def getexif(self): """ Gets EXIF data of the image. - + :returns: an :py:class:`~PIL.Image.Exif` object. """ if self._exif is None: @@ -3608,18 +3608,19 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ Exif class provides read and write access to EXIF image data. - + Only basic information is available on the root level, in Exif object itself. In order to access the rest, obtain their respective IFDs using :py:meth:`~PIL.Image.Exif.get_ifd` method and one of :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and ``GPSInfo``). - + Both root Exif and child IFD objects support dict interface and can be indexed by int values that are available as enum members of :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and :py:class:`~PIL.ExifTags.Interop`. """ + endian = None bigtiff = False From a45211b811f1a0e4313b672c1629bedca857745a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Feb 2023 20:33:08 +1100 Subject: [PATCH 044/155] Updated freetype to 2.13 --- 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 8a4494103..ff95581fa 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -253,9 +253,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.12.1.tar.gz", # noqa: E501 - "filename": "freetype-2.12.1.tar.gz", - "dir": "freetype-2.12.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.0.tar.gz", # noqa: E501 + "filename": "freetype-2.13.0.tar.gz", + "dir": "freetype-2.13.0", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 5059e5c143ee4b9e625163897a5610611a325bef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 08:11:50 +1100 Subject: [PATCH 045/155] Do not raise an error if os.environ does not contain PATH --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 8f7f223f8..0c8b390de 100755 --- a/setup.py +++ b/setup.py @@ -243,6 +243,8 @@ def _find_include_dir(self, dirname, include): def _cmd_exists(cmd): + if "PATH" not in os.environ: + return return any( os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) From 997932bc93c778919fe22f4838ef0929482fe520 Mon Sep 17 00:00:00 2001 From: Marcel Telka Date: Thu, 9 Feb 2023 23:23:29 +0100 Subject: [PATCH 046/155] Update HPND wording in LICENSE file --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 125bdcc44..cf65e86d7 100644 --- a/LICENSE +++ b/LICENSE @@ -13,8 +13,8 @@ By obtaining, using, and/or copying this software and/or its associated documentation, you agree that you have read, understood, and will comply with the following terms and conditions: -Permission to use, copy, modify, and distribute this software and its -associated documentation for any purpose and without fee is hereby granted, +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies, and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of Secret Labs AB or the author not be From a8e03e4dabd0fcb55fec922825dec9861e7ef103 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 20:11:50 +1100 Subject: [PATCH 047/155] Added Exif code examples --- src/PIL/Image.py | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 813c237ad..a280935a5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1433,7 +1433,7 @@ class Image: def getexif(self): """ - Gets EXIF data of the image. + Gets EXIF data from the image. :returns: an :py:class:`~PIL.Image.Exif` object. """ @@ -3607,18 +3607,36 @@ atexit.register(core.clear_cache) class Exif(MutableMapping): """ - Exif class provides read and write access to EXIF image data. + This class provides read and write access to EXIF image data:: - Only basic information is available on the root level, in Exif object - itself. In order to access the rest, obtain their respective IFDs using - :py:meth:`~PIL.Image.Exif.get_ifd` method and one of - :py:class:`~PIL.ExifTags.IFD` members (most notably ``Exif`` and - ``GPSInfo``). + from PIL import Image + im = Image.open("exif.png") + exif = im.getexif() # Returns an instance of this class - Both root Exif and child IFD objects support dict interface and can be - indexed by int values that are available as enum members of - :py:class:`~PIL.ExifTags.Base`, :py:class:`~PIL.ExifTags.GPS`, and - :py:class:`~PIL.ExifTags.Interop`. + Information can be read and written, iterated over or deleted:: + + print(exif[274]) # 1 + exif[274] = 2 + for k, v in exif.items(): + print("Tag", k, "Value", v) # Tag 274 Value 2 + del exif[274] + + To access information beyond IFD0, :py:meth:`~PIL.Image.Exif.get_ifd` + returns a dictionary:: + + from PIL import ExifTags + im = Image.open("exif_gps.jpg") + exif = im.getexif() + gps_ifd = exif.get_ifd(ExifTags.IFD.GPSInfo) + print(gps_ifd) + + Other IFDs include ``ExifTags.IFD.Exif``, ``ExifTags.IFD.Makernote``, + ``ExifTags.IFD.Interop`` and ``ExifTags.IFD.IFD1``. + + :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: + + print(exif[ExifTags.Base.Software]) # PIL + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' """ endian = None From bb524018d35ea6450e56c3cd3db489eeb0f5a79c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 16:20:27 +1100 Subject: [PATCH 048/155] Raise an error when EXIF data is too long --- Tests/test_file_jpeg.py | 5 ++++- src/PIL/JpegImagePlugin.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index e3c5abcbd..b84661330 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -270,7 +270,10 @@ class TestFileJpeg: # https://github.com/python-pillow/Pillow/issues/148 f = str(tmp_path / "temp.jpg") im = hopper() - im.save(f, "JPEG", quality=90, exif=b"1" * 65532) + im.save(f, "JPEG", quality=90, exif=b"1" * 65533) + + with pytest.raises(ValueError): + im.save(f, "JPEG", quality=90, exif=b"1" * 65534) def test_exif_typeerror(self): with Image.open("Tests/images/exif_typeerror.jpg") as im: diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d7ddbe0d9..71ae84c04 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -730,10 +730,10 @@ def _save(im, fp, filename): extra = info.get("extra", b"") + MAX_BYTES_IN_MARKER = 65533 icc_profile = info.get("icc_profile") if icc_profile: ICC_OVERHEAD_LEN = 14 - MAX_BYTES_IN_MARKER = 65533 MAX_DATA_BYTES_IN_MARKER = MAX_BYTES_IN_MARKER - ICC_OVERHEAD_LEN markers = [] while icc_profile: @@ -764,6 +764,9 @@ def _save(im, fp, filename): exif = info.get("exif", b"") if isinstance(exif, Image.Exif): exif = exif.tobytes() + if len(exif) > MAX_BYTES_IN_MARKER: + msg = "EXIF data is too long" + raise ValueError(msg) # get keyword arguments im.encoderconfig = ( From 20daa1d049b8b0e321c282af073e05b114fb216e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Feb 2023 20:49:09 +1100 Subject: [PATCH 049/155] Fixed typo --- src/PIL/TiffTags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 9b5277138..ac048ba56 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -312,7 +312,7 @@ TAGS = { 34910: "HylaFAX FaxRecvTime", 36864: "ExifVersion", 36867: "DateTimeOriginal", - 36868: "DateTImeDigitized", + 36868: "DateTimeDigitized", 37121: "ComponentsConfiguration", 37122: "CompressedBitsPerPixel", 37724: "ImageSourceData", From ac6b9632b4c90ff820c23420e699c68092166d63 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 050/155] Test Python 3.12-dev on macOS and Ubuntu --- .ci/install.sh | 3 ++- .github/workflows/macos-install.sh | 3 ++- .github/workflows/test.yml | 1 + docs/installation.rst | 12 ++++++------ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 518b66acc..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -37,7 +37,8 @@ python3 -m pip install -U pytest-timeout python3 -m pip install pyroma if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy + # TODO Remove condition when NumPy supports 3.12 + if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index dfd7d0553..1fc6262f4 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -13,7 +13,8 @@ python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-timeout python3 -m pip install pyroma -python3 -m pip install numpy +# TODO Remove condition when NumPy supports 3.12 +if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # extra test images pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c7b77be..8e06de4cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: python-version: [ "pypy3.9", "pypy3.8", + "3.12-dev", "3.11", "3.10", "3.9", diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..93260e141 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -442,15 +442,15 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 11 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | +| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | -| | PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | arm64v8, ppc64le, | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| | 3.12, PyPy3 | | +| +----------------------------+---------------------+ +| | 3.10 | arm64v8, ppc64le, | | | | s390x, x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | From 826c98156ceaf619493af22808189500f9b0ae46 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 16:36:13 +0200 Subject: [PATCH 051/155] Remove unused listwindows functions for Windows/3.12 support --- src/_imaging.c | 3 --- src/display.c | 73 -------------------------------------------------- 2 files changed, 76 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 05e1370f6..cece2e93a 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3984,8 +3984,6 @@ PyImaging_GrabScreenWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args); extern PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args); -extern PyObject * PyImaging_EventLoopWin32(PyObject *self, PyObject *args); extern PyObject * PyImaging_DrawWmf(PyObject *self, PyObject *args); @@ -4069,7 +4067,6 @@ static PyMethodDef functions[] = { {"grabclipboard_win32", (PyCFunction)PyImaging_GrabClipboardWin32, METH_VARARGS}, {"createwindow", (PyCFunction)PyImaging_CreateWindowWin32, METH_VARARGS}, {"eventloop", (PyCFunction)PyImaging_EventLoopWin32, METH_VARARGS}, - {"listwindows", (PyCFunction)PyImaging_ListWindowsWin32, METH_VARARGS}, {"drawwmf", (PyCFunction)PyImaging_DrawWmf, METH_VARARGS}, #endif #ifdef HAVE_XCB diff --git a/src/display.c b/src/display.c index 0ce10e249..a50fc3e24 100644 --- a/src/display.c +++ b/src/display.c @@ -421,79 +421,6 @@ error: return NULL; } -static BOOL CALLBACK -list_windows_callback(HWND hwnd, LPARAM lParam) { - PyObject *window_list = (PyObject *)lParam; - PyObject *item; - PyObject *title; - RECT inner, outer; - int title_size; - int status; - - /* get window title */ - title_size = GetWindowTextLength(hwnd); - if (title_size > 0) { - title = PyUnicode_FromStringAndSize(NULL, title_size); - if (title) { - GetWindowTextW(hwnd, PyUnicode_AS_UNICODE(title), title_size + 1); - } - } else { - title = PyUnicode_FromString(""); - } - if (!title) { - return 0; - } - - /* get bounding boxes */ - GetClientRect(hwnd, &inner); - GetWindowRect(hwnd, &outer); - - item = Py_BuildValue( - F_HANDLE "N(iiii)(iiii)", - hwnd, - title, - inner.left, - inner.top, - inner.right, - inner.bottom, - outer.left, - outer.top, - outer.right, - outer.bottom); - if (!item) { - return 0; - } - - status = PyList_Append(window_list, item); - - Py_DECREF(item); - - if (status < 0) { - return 0; - } - - return 1; -} - -PyObject * -PyImaging_ListWindowsWin32(PyObject *self, PyObject *args) { - PyObject *window_list; - - window_list = PyList_New(0); - if (!window_list) { - return NULL; - } - - EnumWindows(list_windows_callback, (LPARAM)window_list); - - if (PyErr_Occurred()) { - Py_DECREF(window_list); - return NULL; - } - - return window_list; -} - /* -------------------------------------------------------------------- */ /* Windows clipboard grabber */ From ab2809a44c2211741ce2eca132d9cc60619ea2b5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 10 Feb 2023 11:11:39 +0200 Subject: [PATCH 052/155] Test Python 3.12-dev on macOS and Ubuntu --- .github/workflows/test-windows.yml | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index e938e999d..306e34ca9 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows diff --git a/docs/installation.rst b/docs/installation.rst index 93260e141..1bfc65f4b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -456,7 +456,7 @@ These platforms are built and tested for every change. | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | -| | PyPy3 | | +| | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | | +----------------------------+---------------------+ From f6040bc8792e355a0b62ba961093cde8127d39d4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 11 Feb 2023 23:14:47 +0200 Subject: [PATCH 053/155] Docker tests: enable gcov support for codecov/codecov-action --- .github/workflows/test-docker.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7331cf8ee..7d2b20d65 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -87,6 +87,7 @@ jobs: with: flags: GHA_Docker name: ${{ matrix.docker }} + gcov: true success: permissions: From 0836c747f08fdfcdfd9be5ee858d3ad10bb5430f Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 11 Feb 2023 23:16:13 +0000 Subject: [PATCH 054/155] add gcov coverage to test.yml --- .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 11c7b77be..87cad1f36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,9 +107,9 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 with: - file: ./coverage.xml flags: ${{ matrix.os == 'macos-latest' && 'GHA_macOS' || 'GHA_Ubuntu' }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} + gcov: true success: permissions: From 42683781d6d25913cf9274dd7cd14153e542cfd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 13:41:35 +1100 Subject: [PATCH 055/155] Updated CI targets --- docs/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1bfc65f4b..1b5719a8e 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -447,11 +447,13 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.8 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | -| | | s390x, x86-64 | +| | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ From d9085541bf9ef82be182f209931e5326629df055 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 15:18:53 +1100 Subject: [PATCH 056/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e35a55965..626860e71 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if EXIF data is too long when saving JPEG #6939 + [radarhere] + - Handle more than one directory returned by pkg-config #6896 [sebastic, radarhere] From da38395396c2f6322073a0f24a38b5780e46e89f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Feb 2023 21:56:23 +1100 Subject: [PATCH 057/155] Removed quotes from result in docstring --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a280935a5..63bad83a1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3636,7 +3636,7 @@ class Exif(MutableMapping): :py:mod:`~PIL.ExifTags` also has enum classes to provide names for data:: print(exif[ExifTags.Base.Software]) # PIL - print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # '1999:99:99 99:99:99' + print(gps_ifd[ExifTags.GPS.GPSDateStamp]) # 1999:99:99 99:99:99 """ endian = None From bbbb8e6e21108b13a3ae978b4aa0b783aa10bddf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Feb 2023 22:05:51 +1100 Subject: [PATCH 058/155] Updated harfbuzz to 7.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 ff95581fa..bef8afa9d 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/6.0.0.zip", - "filename": "harfbuzz-6.0.0.zip", - "dir": "harfbuzz-6.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", + "filename": "harfbuzz-7.0.0.zip", + "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From ad0e9dbaaf41e5c7abddf08d850cf0c0d98a3a92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 14 Feb 2023 10:52:32 +1100 Subject: [PATCH 059/155] Fixed writing int as UNDEFINED tag --- Tests/test_file_tiff_metadata.py | 16 ++++++++++++++++ src/PIL/TiffImagePlugin.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index a4481d85f..fdabae3a3 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -216,6 +216,22 @@ def test_writing_other_types_to_bytes(value, tmp_path): assert reloaded.tag_v2[700] == b"\x01" +def test_writing_other_types_to_undefined(tmp_path): + im = hopper() + info = TiffImagePlugin.ImageFileDirectory_v2() + + tag = TiffTags.TAGS_V2[33723] + assert tag.type == TiffTags.UNDEFINED + + info[33723] = 1 + + out = str(tmp_path / "temp.tiff") + im.save(out, tiffinfo=info) + + with Image.open(out) as reloaded: + assert reloaded.tag_v2[33723] == b"1" + + def test_undefined_zero(tmp_path): # Check that the tag has not been changed since this test was created tag = TiffTags.TAGS_V2[45059] diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2cf5b173f..aaaf8fcb9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -764,6 +764,8 @@ class ImageFileDirectory_v2(MutableMapping): @_register_writer(7) def write_undefined(self, value): + if isinstance(value, int): + value = str(value).encode("ascii", "replace") return value @_register_loader(10, 8) From b7630bd675bd260dfac5eb53bc23afe1ab8e09d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Feb 2023 11:41:32 +1100 Subject: [PATCH 060/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626860e71..aa03cfc40 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Fixed writing int as UNDEFINED tag #6950 + [radarhere] + - Raise an error if EXIF data is too long when saving JPEG #6939 [radarhere] From 06ba226e7b6b9fbcbf74839bf123b4095d6f9c92 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:28:16 -0800 Subject: [PATCH 061/155] image-file-formats.rst: correct WebP quality range 0-100, not 1-100 --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..56937b5c7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1126,7 +1126,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: If present and true, instructs the WebP writer to use lossless compression. **quality** - Integer, 1-100, Defaults to 80. For lossy, 0 gives the smallest + Integer, 0-100, Defaults to 80. For lossy, 0 gives the smallest size and 100 the largest. For lossless, this parameter is the amount of effort put into the compression: 0 is the fastest, but gives larger files compared to the slowest, but best, 100. From 8935dad32e31a121906a92bfc3eacdc3e410a711 Mon Sep 17 00:00:00 2001 From: James Zern Date: Tue, 14 Feb 2023 17:29:06 -0800 Subject: [PATCH 062/155] image-file-formats.rst: document WebP 'xmp' option --- docs/handbook/image-file-formats.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..a85ca59dd 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1147,6 +1147,10 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: The exif data to include in the saved file. Only supported if the system WebP library was built with webpmux support. +**xmp** + The XMP data to include in the saved file. Only supported if + the system WebP library was built with webpmux support. + Saving sequences ~~~~~~~~~~~~~~~~ From 0f2a4c1ae5d8492280af126dc7fda9eeef9b9d50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Feb 2023 19:19:17 +1100 Subject: [PATCH 063/155] Added "corners" argument to rounded_rectangle() --- ...agedraw_rounded_rectangle_corners_nnnn.png | Bin 0 -> 544 bytes ...agedraw_rounded_rectangle_corners_nnny.png | Bin 0 -> 685 bytes ...agedraw_rounded_rectangle_corners_nnyn.png | Bin 0 -> 649 bytes ...agedraw_rounded_rectangle_corners_nnyy.png | Bin 0 -> 755 bytes ...agedraw_rounded_rectangle_corners_nynn.png | Bin 0 -> 643 bytes ...agedraw_rounded_rectangle_corners_nyny.png | Bin 0 -> 775 bytes ...agedraw_rounded_rectangle_corners_nyyn.png | Bin 0 -> 741 bytes ...agedraw_rounded_rectangle_corners_nyyy.png | Bin 0 -> 844 bytes ...agedraw_rounded_rectangle_corners_ynnn.png | Bin 0 -> 656 bytes ...agedraw_rounded_rectangle_corners_ynny.png | Bin 0 -> 785 bytes ...agedraw_rounded_rectangle_corners_ynyn.png | Bin 0 -> 752 bytes ...agedraw_rounded_rectangle_corners_ynyy.png | Bin 0 -> 856 bytes ...agedraw_rounded_rectangle_corners_yynn.png | Bin 0 -> 737 bytes ...agedraw_rounded_rectangle_corners_yyny.png | Bin 0 -> 870 bytes ...agedraw_rounded_rectangle_corners_yyyn.png | Bin 0 -> 835 bytes ...agedraw_rounded_rectangle_corners_yyyy.png | Bin 0 -> 934 bytes Tests/test_imagedraw.py | 30 +++++ docs/reference/ImageDraw.rst | 2 + src/PIL/ImageDraw.py | 106 ++++++++++++------ 19 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nnyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_nyyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yynn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyny.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_corners_yyyy.png diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnnn.png new file mode 100644 index 0000000000000000000000000000000000000000..3e79e21aedb620a6d057f927610e1f1a791fd5da GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV0`ZB;uumf=j|0kzC#W?3d&6b&3+Jg_Su>JzjK?ym2`v- zDJkj*<%_Sao1Z3b)A4uC?n|AnP7x|CqXb1^;MZ!(KL6Y1T^!TqfCBT;qYZc3<~;7% zpmv|-{P9qaM|(CrdocO-gvmhJbv&mR9xY67H$BamTX;lO{hAK@mZM_tBUJQwvZpcg z>4wGDC7msDh^{HW8ftO6@$ZRl=Ii^C&z_pG?Z=nq>E9zIFN?|7eOp}+b}Xj*P1)|h zy!(>-eD}%EeY#Pnzb7*K%f?S;{CDaOsJGfS?rtxAWoWbf=A4}`z-oFP+I6(Ttoqx>dPD}f)_xZ95B<$(x=d#Wz Gp$P!^IRgOz literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nnyn.png new file mode 100644 index 0000000000000000000000000000000000000000..d825ad2631717dc05270f8c211bae1793ce1c256 GIT binary patch literal 649 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&YbaSW-L^Y-S&pv49Z4i~R{ zJpB3juLnDJYq741xqiwrUsdwNVOgdU9-tN&_%TyY`tfILv4eYOg}ck`%NLx}xxs^@ zyJdri%{`OfnP#gW_{BWCH}hSDN{g%0C_!NuyeXZTUw?k7=XywB9@TsQ{h;olN5RjI z?Er;(uK8a6eF9>fi5nkYJN`}lP|{haQ2BlxL9y`KU8@vzlny1`49^R9j?np)6q?*~ z-1*wU^&9{2?Wic2YAseT8+uhhtX?MK>IPAxFuTS$>wV1|&SgKI{Oi{XvBbIO*6q*u zb|!MaS1seaGd9mmAKNWxb|y(>j|^&kMy5PUPrr z+2Fy^edK;dSP5 z4k>N4((yl>994Z^yk%>VQoBK(%y$iyjXG;SKApV$;l(w-sy|GY?LKmBLr&E3jc?=h z`CcdO+SZrwOfSC9IQ?v2sbOIB{@c%1slAVI32QUmSD$$K*V=bC*DcevzIL=E_pe=f zn5pmO%*UsSPuHx8nJsRz-}g*i<<)EDYj#e}iG829e$}$ward5IyL|2Ut&OWPe=q)U z?T}n|{*&WxA1r@A{mh+zms3w`ze&AYu_wm9s`y9j{nX4Xyte+pcH?+&cbiHAvXg)z4*}Q$iB}g)$Z3 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nynn.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e95d487498b4dec519e1b2e3745d5d1bda62f2 GIT binary patch literal 643 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVDj>GaSW-L^Y-RJze5fp4uJ-B zKmJ(e`LN_>a_{Y^IJe1oW@6!IjfVw59Wc=Fyd)+}(xUT@FYN7BUz>5O#4|#r<*4tmL%TLaq`ubP1CnXk z5RrX3-#KxjfEec}K~WfVe)rwozxb!f9vNX^u!6%DmLh<3MVS20FPfX2{qNa=L_J;o KT-G@yGywp+4$-0j literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyny.png new file mode 100644 index 0000000000000000000000000000000000000000..274d27984dcb7633223d2557d9d3cb44f109f99f GIT binary patch literal 775 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU^?gN;uumf=k3jdMT;Cn90KR+ zzqc##~$aFue*C+9jjj@ z{aLwZm9B6%|FOh!qm4fjWXhJ;3coJ8bFgOW=NknVr)B5O-}IUxaqpz5hw|R9yH}I? z|JS=iRi53gx@+?v$aY^&)#>TEc{I1>sQvM6syhFyH|Qo7rvBUbqcXL3!;Fs=(T9@m zHRp16w`_>`a^ixJ*!DYjr)`KRyuUxX#nmZd&1}B4hUFJc6DJCYl}l>>K9n?h@A?mi zloWM@4kfLQ-Ot+!RHOn@IZD97;MyUn@cHMGzbVZy+UUX2edOH+8{V5q&JoK0h4#qx zI|` v(89Ixy0atZ_db_BI=N^4Kcr-NAp1ABZ;XS5VFODSD5ZM3`njxgN@xNAqD46+ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_nyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..c5f40bfdbdd968e3392c9ec9e4103595519afe01 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qqp;uumf=j~0$pxXuv4uO|n z9xN^1)gZ>y_f+Lyz@+bG6Ky7m*QcmQiUIY&zz=&{-)G0e@36#Y)f`njRF$(h>PPri z1N%cOZWSecJZF)ksxJ1v=Wy7rH%F@M@{;$RzI>zb;=Jr_9iOS3Tv{NX`0>?^Z7oMNa8$bSDxl1NnTV^80#9uEDEwmQN7u@%rJLU)^cvO>ghMBK_fTPx7>n zpUzo7-Z0gM%X6MLI6Xx~^rzQ+=Y^#>ApPbGyT~5PQvq{CEI^{3u6{1-oD!M5dtaSW-L^Y*59(JcoNhd@j1 z_j9eARYVP#YS-Hroa6D_v#cvxH_=}hs09XU{PVe=A5X90u)F%B$F=3_EjQC2;ifa> z6GL)uZrowuH#_C{qeB+Cm$u2i{=293r&fOQQP=Esm3vMtV{Q5BBgR!-Q&#xn(9hQ| z4^?@UbMG#%VLcz=^ZxOg|Trg8C01Z?}J@_v?6{t5bxES^LwxAG5aAz1Kc; z%VOem1F>_vMJGjwU)xay6wfmIEBEJcpKDTjgxT8b8{hAalf8DfBzJGq%>19Qyf<9R7W$>ejEFervLB)oaH@-N^mOzH_(i9qWB*&;D&+ z7af;)|JtFqPe0x~`mp`p-lxtQZ)Qh)-LdJ}tmCU?WD7FqSoW+pJSSQFD*oQ_nTLNc zJ>KHE{9VL!H`5uPdq2CUCH^(M^-;!j)xpAu>BpYy9#ww4+8&m$f%E~d{fw-szSh0M QadSYzp00i_>zopr09Uk5F8}}l literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynnn.png new file mode 100644 index 0000000000000000000000000000000000000000..efd27be4f9df7c8c7733920dbe1330f938a4c2f5 GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU<&tiaSW-L^Y+HUzFQ6=42~V& z4&R@z-mWq6+Q%l|Jf@thXZt6NNuH)Se~<8C=xUyF(VRD4)!-kr*A>ui+Pbn{=n{Jo?+Vzb?~ z%jV_r5u1%Bi!9%5cP~A4nO5Cy(>0MZS9a~sv*Jp(oFKyeyL?R~U&zt@Wo@RX8&^e? zzOaA);rP4_X_gyeKTbWHc(!PPmfnt!8X=<7e;!lXsJrH8N$T!LTGnEnyotX5g!at8 zYdx)ag04)fN{j2GsEE%mPg$ODE7K7=q@*}XP!t9qH)emWoVESI+Z!7_IJ#RlcyMsT f5(fiAL;6?lyA?{unZGam3ljBo^>bP0l+XkKcMjwy literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynny.png new file mode 100644 index 0000000000000000000000000000000000000000..d3acd01abe04c4577f0b930dda7611f10912112c GIT binary patch literal 785 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV7lSy;uumf=k3jddAA%y90FJT zdiehQZPzO+EZBH{Olq04`SHz98+ObpJn;~y2M*#df2{OA`+3?U&u4F@-k-nKV7)GP z_A2(zx^HB!z4>FaZ)JS7UCy@}iTk(wtgprFe|o;H`p2Wb&0o*i9@@2I#=N>^?T2T5 z-8ipq$MHk6rp{bqVe{paowjl4$7lC8q@^y?x_7r!Tg)=9z5wk!>5EJ(>KrYxSE56|)x8pVJ?F zy|X9TlKtkPKo6$~m36JA>htF`=cF80Qq&PTG)f?a!SzG4Ci@?c+{-x)7&ZVun{U$k#q-Iko=zcS`+7x6cm|Cbe>IvE#L^%Ke literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyn.png new file mode 100644 index 0000000000000000000000000000000000000000..55ddbc033fbeea4f13492b24372bd501975c994e GIT binary patch literal 752 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVA}5K;uumf=j~0$pv4Xh4uNw% zJ*fRWvvsN81P0F^7iWAgGYL8TDBN7}G7C@-4A@^j{Ws6_UAgwVlsUGaf5ngo}jmc@sq*v}tt{#E=W#o^opr*NOcHF(=hi0s*D9(*YPV@<_*^#9!^O@1pu=r<}=`-e4k5+RZmio*X zy79#Y{r4ZexoEl0O?*YWQY6yr?X`0Z{~-R&N_w0yHDfj-n?#ImXMfW;>I`Ao&p)&d26y} zExYbKuQ@*Xr}=|N545z!=J%}v%EtIlAR*N!ef5 z&Ch3j-F?OU!NH#7WgkDCt3Q@#e3Q%bn`)x%_CQ~qHP4Or{bqzEJ|JE8i~Wb8(fz*) Q5l29xp00i_>zopr07Mr|_y7O^ literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png b/Tests/images/imagedraw_rounded_rectangle_corners_ynyy.png new file mode 100644 index 0000000000000000000000000000000000000000..c000b26e9671cd748319fbab7e8921d6a98ef3ae GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIV3zlEaSW-L^Y+Hwyu}70Zh=O# z*zNQ0?Ob7%)xpg6=V|}ecLghCJ9THL%>wFyfd{6sa@#{}^EBm_ysP{4J(BzL&7{Q8 ztfYH~UTnzvP_t*Hy|Vi1iIYEGV|yF1weEEIaoasRyuaSFR9bWN=$8_|_QR{bZJ6ix zE+XQp#AK1n3#Wh2>RH*9UwwGhu^Fp=~sI?t3b; zqg%ov^T3lN;bO?wE9vT&d1;~-8h|<#1mCtAq6q#oO#|zgwmDKB6UuXZ15?v$g)EDZ6KVdjnSY zS!PYvtYxRqO^rTXlN&Qz{K`GGSEQ&`{hr~cvU_Ft|??`t^}=W_kE*LmC1t7Yas*e+!$ zc|I_CndGn2)xC3i)K?@L^WNQ9xBAMFp76)Zds@G)-E_G)WT_fo``2h-dY*P=^L|(& c2htA9>tEb*3t(Q@KN}?K>FVdQ&MBb@08oo#djJ3c literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yynn.png new file mode 100644 index 0000000000000000000000000000000000000000..7056b4fd9347bcce6ce3ddc9d0735efd12b5d0c4 GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIU|Qkn;uumf=j{#ezDEus42}|K z559h8ygKfJ*@Ji`9zkoLk6b5L8r_}F4b%bydzQa`y-jNU?JoCY+Gd@HrQTM2le+pq zXH6jQvx>(*Cc3wq^36N$-FsN=uVv09*U;zYe6vd~behi7|G7PZSHD1(TTD0dbH((% z%=2nFZTGHKZ?&!a{_;cY{C)fP&hvYcm-v01{>+_!7nhq&yW)BFxoLG_qpbbC=i5TA zR-V1T`(`(H4sUjzedye~jk&p1B7S9)uC&jr)bU$yv*q8BSvd<OMTnC*-R6jXT$lxi4I`@A$Urx8`Ddczh7^+(T08hSCOU69s?-BP_WwFnn0|kNK9j%}TLfU4=a(s-@L${y(L<-PHR0aqqKv#(y{8_F)g*yZ>hHvbkkzcGjO>FZ%rh)A7i{%d@KU z)_;1#@_k>+yt*~kt#|iTm48}Z_v_cEsw(Ah?%ihl(#{>P|8nZkq^jW4mAZWEJ{&py z`_iOURq?0K=M@&Tx*v{;@(+EVcYK?i6_{80-+gHmAnw+#}zDbA7H(5h`ZwPfb#KMH7Ahss6C(a}s(D3O+yQ zcbk$YRv75a7Tdm-FZ&$(mZQAUWosjI< z#yxIM#TVIZv140~#{2KtbN*LI%ynd{gTe~DWM4fqXlD9 literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png b/Tests/images/imagedraw_rounded_rectangle_corners_yyyn.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1f003440063facdb2a8ae9bb6455d62b5f1b0b GIT binary patch literal 835 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k2}mkgS)OEIVCM34aSW-L^Y-TLqFV+G4vxz& z^IxC8oqy&QUxR%O{Ja(4d2ddf@llfVgb5>16BHcy{^i?_rNGpZ6=G|?~y;~v@w?<{9xN)&gT%XRZ{mHR93szk>{_Lag zF7}q^by8XA-c{DOt-eez|FJ=R&BxLMWnXW-ySc@zz0`2=s&fyXZOgOa3N4k}Zks)S zqel*Rc1>04ZmZer{cFzL{W4Rw`$*dcw|{CCcF9kFy^}p{S>abbfA``EEgL-6w3^!O zE#jMZbKU#4G995qN{XWdN*HXblAa&mzjL-7M>jCIIJ$lJ&1=qXIqG}tRB7Tw0kPL* zwemIUz%WZXdawWO`JO{=uCm%MWtMrNFZA{Hn$kOmx$mrcylh_ev~WwS zgty)=UgoY9+4kq%s}Iie>-O!L=l3Kt@%*~)XYTyJsLt*EYRRoiQ~ffF50Ce5U9#%j z>c4MhiCe$nUA=YQs&jb>(YyN&-SnFD%b+*8urBrDH`(lM4Xf<@AJ06uUgws?wVYk6 z?5-Z%*7onYY+VlLYB~KI>+Eh%^j&Se=B7(%?7?^2%H&#C?dq#Ne=A={>DQrEyTiAw zUR_!9=<2SQ^JKd_`Zj3&Q?-~MRayVJ@2Sk5Woysh4fJT)&=JAC`dyqQ`}&;A_uI;J zlv-SYVgh1pg*rZu#DecshQo!%8SzkmVTE(;2wjg~sgADZUY zzLQPNopVC<%BkOdPLLSzJG}blm+ueEt;Gbb4{yqhJ-#pft}HMnx2;?7amw@XsBLc| zHh13om9u;GtUt$;Qj#OSRvcXRb!A^p_~m)B*=DiP^PeY#UN*T~w(d^Uv+T#W_ALBn zc5%;+jQ9C#u6b=+w^=&$YRQY^)!DYWbFUv*wd}9^!&`^?jtW=qlC{}=&$#%%`|oMt zH*QB+#!b5)D_i&Ca`e8IL-$;+pPvdzun)FNS@!G?Og_f*_k4A)%rW^DiIdyjMeGmF zGI)IT$Ha{r_Jx+&xQAvHSgg4Z&K>`Lal^6= x1 - x0 - if full_x: - # The two left and two right corners are joined - d = x1 - x0 - full_y = d >= y1 - y0 - if full_y: - # The two top and two bottom corners are joined - d = y1 - y0 - if full_x and full_y: - # If all corners are joined, that is a circle - return self.ellipse(xy, fill, outline, width) + full_x, full_y = False, False + if all(corners): + full_x = d >= x1 - x0 + if full_x: + # The two left and two right corners are joined + d = x1 - x0 + full_y = d >= y1 - y0 + if full_y: + # The two top and two bottom corners are joined + d = y1 - y0 + if full_x and full_y: + # If all corners are joined, that is a circle + return self.ellipse(xy, fill, outline, width) - if d == 0: - # If the corners have no curve, that is a rectangle + if d == 0 or not any(corners): + # If the corners have no curve, + # or there are no corners, + # that is a rectangle return self.rectangle(xy, fill, outline, width) r = d // 2 @@ -338,12 +346,17 @@ class ImageDraw: ) else: # Draw four separate corners - parts = ( - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), - ((x0, y0, x0 + d, y0 + d), 180, 270), - ) + parts = [] + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) + ): + if corners[i]: + parts.append(part) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -358,25 +371,50 @@ class ImageDraw: else: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill, 1) if not full_x and not full_y: - self.draw.draw_rectangle((x0, y0 + r + 1, x0 + r, y1 - r - 1), fill, 1) - self.draw.draw_rectangle((x1 - r, y0 + r + 1, x1, y1 - r - 1), fill, 1) + left = [x0, y0, x0 + r, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, fill, 1) + + right = [x1 - r, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, fill, 1) if ink is not None and ink != fill and width != 0: draw_corners(False) if not full_x: - self.draw.draw_rectangle( - (x0 + r + 1, y0, x1 - r - 1, y0 + width - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x0 + r + 1, y1 - width + 1, x1 - r - 1, y1), ink, 1 - ) + top = [x0, y0, x1, y0 + width - 1] + if corners[0]: + top[0] += r + 1 + if corners[1]: + top[2] -= r + 1 + self.draw.draw_rectangle(top, ink, 1) + + bottom = [x0, y1 - width + 1, x1, y1] + if corners[3]: + bottom[0] += r + 1 + if corners[2]: + bottom[2] -= r + 1 + self.draw.draw_rectangle(bottom, ink, 1) if not full_y: - self.draw.draw_rectangle( - (x0, y0 + r + 1, x0 + width - 1, y1 - r - 1), ink, 1 - ) - self.draw.draw_rectangle( - (x1 - width + 1, y0 + r + 1, x1, y1 - r - 1), ink, 1 - ) + left = [x0, y0, x0 + width - 1, y1] + if corners[0]: + left[1] += r + 1 + if corners[3]: + left[3] -= r + 1 + self.draw.draw_rectangle(left, ink, 1) + + right = [x1 - width + 1, y0, x1, y1] + if corners[1]: + right[1] += r + 1 + if corners[2]: + right[3] -= r + 1 + self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): """Draw text.""" From 60208a325083579361eaecc6d388e265aa52bea6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Feb 2023 10:32:55 +1100 Subject: [PATCH 064/155] Only allow "corners" to be used as a keyword argument --- src/PIL/ImageDraw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index dc77c06ea..a55ebbe8e 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -296,7 +296,7 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, corners=None + self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None ): """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): From a55c2b42b9fdeb395cdc387ea768eb12e9a547bd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 18 Feb 2023 20:34:52 +1100 Subject: [PATCH 065/155] If following colon, replace Python code-blocks with double colons --- docs/deprecations.rst | 16 +++-------- docs/handbook/image-file-formats.rst | 4 +-- .../writing-your-own-image-plugin.rst | 12 ++------ docs/reference/Image.rst | 28 +++++-------------- docs/reference/ImagePath.rst | 4 +-- docs/reference/ImageWin.rst | 4 +-- docs/reference/open_files.rst | 4 +-- docs/releasenotes/6.1.0.rst | 12 ++------ docs/releasenotes/7.0.0.rst | 12 ++------ docs/releasenotes/9.0.0.rst | 4 +-- docs/releasenotes/9.2.0.rst | 8 ++---- winbuild/build.rst | 4 +-- 12 files changed, 28 insertions(+), 84 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..0db19a64e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -177,9 +177,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -194,9 +192,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont @@ -336,16 +332,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..d6b42589a 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1393,9 +1393,7 @@ WMF, EMF Pillow can identify WMF and EMF files. On Windows, it can read WMF and EMF files. By default, it will load the image -at 72 dpi. To load it at another resolution: - -.. code-block:: python +at 72 dpi. To load it at another resolution:: from PIL import Image diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 59dfac588..75604e17a 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -108,9 +108,7 @@ Note that the image plugin must be explicitly registered using :py:func:`PIL.Image.register_open`. Although not required, it is also a good idea to register any extensions used by this format. -Once the plugin has been imported, it can be used: - -.. code-block:: python +Once the plugin has been imported, it can be used:: from PIL import Image import SpamImagePlugin @@ -169,9 +167,7 @@ The raw decoder The ``raw`` decoder is used to read uncompressed data from an image file. It can be used with most uncompressed file formats, such as PPM, BMP, uncompressed TIFF, and many others. To use the raw decoder with the -:py:func:`PIL.Image.frombytes` function, use the following syntax: - -.. code-block:: python +:py:func:`PIL.Image.frombytes` function, use the following syntax:: image = Image.frombytes( mode, size, data, "raw", @@ -281,9 +277,7 @@ decoder that can be used to read various packed formats into a floating point image memory. To use the bit decoder with the :py:func:`PIL.Image.frombytes` function, use -the following syntax: - -.. code-block:: python +the following syntax:: image = Image.frombytes( mode, size, data, "bit", diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index ad0abbbd9..976b148fc 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -127,9 +127,7 @@ methods. Unless otherwise stated, all methods return a new instance of the .. automethod:: PIL.Image.Image.convert The following example converts an RGB image (linearly calibrated according to -ITU-R 709, using the D65 luminant) to the CIE XYZ color space: - -.. code-block:: python +ITU-R 709, using the D65 luminant) to the CIE XYZ color space:: rgb2xyz = ( 0.412453, 0.357580, 0.180423, 0, @@ -140,9 +138,7 @@ ITU-R 709, using the D65 luminant) to the CIE XYZ color space: .. automethod:: PIL.Image.Image.copy .. automethod:: PIL.Image.Image.crop -This crops the input image with the provided coordinates: - -.. code-block:: python +This crops the input image with the provided coordinates:: from PIL import Image @@ -162,9 +158,7 @@ This crops the input image with the provided coordinates: .. automethod:: PIL.Image.Image.entropy .. automethod:: PIL.Image.Image.filter -This blurs the input image using a filter from the ``ImageFilter`` module: - -.. code-block:: python +This blurs the input image using a filter from the ``ImageFilter`` module:: from PIL import Image, ImageFilter @@ -176,9 +170,7 @@ This blurs the input image using a filter from the ``ImageFilter`` module: .. automethod:: PIL.Image.Image.frombytes .. automethod:: PIL.Image.Image.getbands -This helps to get the bands of the input image: - -.. code-block:: python +This helps to get the bands of the input image:: from PIL import Image @@ -187,9 +179,7 @@ This helps to get the bands of the input image: .. automethod:: PIL.Image.Image.getbbox -This helps to get the bounding box coordinates of the input image: - -.. code-block:: python +This helps to get the bounding box coordinates of the input image:: from PIL import Image @@ -217,9 +207,7 @@ This helps to get the bounding box coordinates of the input image: .. automethod:: PIL.Image.Image.remap_palette .. automethod:: PIL.Image.Image.resize -This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``: - -.. code-block:: python +This resizes the given image from ``(width, height)`` to ``(width/2, height/2)``:: from PIL import Image @@ -231,9 +219,7 @@ This resizes the given image from ``(width, height)`` to ``(width/2, height/2)`` .. automethod:: PIL.Image.Image.rotate -This rotates the input image by ``theta`` degrees counter clockwise: - -.. code-block:: python +This rotates the input image by ``theta`` degrees counter clockwise:: from PIL import Image diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index b9bdfc507..7c1a3ad70 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -60,9 +60,7 @@ vector data. Path objects can be passed to the methods on the .. py:method:: PIL.ImagePath.Path.transform(matrix) Transforms the path in place, using an affine transform. The matrix is a - 6-tuple (a, b, c, d, e, f), and each point is mapped as follows: - - .. code-block:: python + 6-tuple (a, b, c, d, e, f), and each point is mapped as follows:: xOut = xIn * a + yIn * b + c yOut = xIn * d + yIn * e + f diff --git a/docs/reference/ImageWin.rst b/docs/reference/ImageWin.rst index 2ee3cadb7..4151be4a7 100644 --- a/docs/reference/ImageWin.rst +++ b/docs/reference/ImageWin.rst @@ -9,9 +9,7 @@ Windows. ImageWin can be used with PythonWin and other user interface toolkits that provide access to Windows device contexts or window handles. For example, -Tkinter makes the window handle available via the winfo_id method: - -.. code-block:: python +Tkinter makes the window handle available via the winfo_id method:: from PIL import ImageWin diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index 6bfd50588..f31941c9a 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -61,9 +61,7 @@ Image Lifecycle * ``Image.Image.close()`` Closes the file and destroys the core image object. The Pillow context manager will also close the file, but will not destroy - the core image object. e.g.: - -.. code-block:: python + the core image object. e.g.:: with Image.open("test.jpg") as img: img.load() diff --git a/docs/releasenotes/6.1.0.rst b/docs/releasenotes/6.1.0.rst index eb4304843..76e13b061 100644 --- a/docs/releasenotes/6.1.0.rst +++ b/docs/releasenotes/6.1.0.rst @@ -13,16 +13,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been dep Use a context manager or call ``Image.close()`` instead to close the file in a deterministic way. -Deprecated: - -.. code-block:: python +Deprecated:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") @@ -79,9 +75,7 @@ Image quality for JPEG compressed TIFF The TIFF encoder accepts a ``quality`` parameter for ``jpeg`` compressed TIFF files. A value from 0 (worst) to 100 (best) controls the image quality, similar to the JPEG -encoder. The default is 75. For example: - -.. code-block:: python +encoder. The default is 75. For example:: im.save("out.tif", compression="jpeg", quality=85) diff --git a/docs/releasenotes/7.0.0.rst b/docs/releasenotes/7.0.0.rst index 80002b0ce..f2e235289 100644 --- a/docs/releasenotes/7.0.0.rst +++ b/docs/releasenotes/7.0.0.rst @@ -118,9 +118,7 @@ Loading WMF images at a given DPI ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ On Windows, Pillow can read WMF files, with a default DPI of 72. An image can -now also be loaded at another resolution: - -.. code-block:: python +now also be loaded at another resolution:: from PIL import Image with Image.open("drawing.wmf") as im: @@ -136,16 +134,12 @@ Implicitly closing the image's underlying file in ``Image.__del__`` has been rem Use a context manager or call :py:meth:`~PIL.Image.Image.close` instead to close the file in a deterministic way. -Previous method: - -.. code-block:: python +Previous method:: im = Image.open("hopper.png") im.save("out.jpg") -Use instead: - -.. code-block:: python +Use instead:: with Image.open("hopper.png") as im: im.save("out.jpg") diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index a19da361a..616cf4aa3 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -155,9 +155,7 @@ altered slightly with this change. Added support for pickling TrueType fonts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TrueType fonts may now be pickled and unpickled. For example: - -.. code-block:: python +TrueType fonts may now be pickled and unpickled. For example:: import pickle from PIL import ImageFont diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 6dbfa2702..3dfb25840 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -59,9 +59,7 @@ Deprecated Use :py:meth:`.ImageDraw2.Draw.textsize` :py:meth:`.ImageDraw2.Draw.textbbox` and :py:meth:`.ImageDraw2.Draw.textlength` =========================================================================== ============================================================================================================= -Previous code: - -.. code-block:: python +Previous code:: from PIL import Image, ImageDraw, ImageFont @@ -76,9 +74,7 @@ Previous code: width, height = font.getsize_multiline("Hello\nworld") width, height = draw.multiline_textsize("Hello\nworld") -Use instead: - -.. code-block:: python +Use instead:: from PIL import Image, ImageDraw, ImageFont diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..d4275a274 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -96,9 +96,7 @@ directory. Example ------- -The following is a simplified version of the script used on AppVeyor: - -.. code-block:: +The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild From 43682de4bdb6c5f93340107386f86becbfbad25a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Feb 2023 08:12:57 +1100 Subject: [PATCH 066/155] Updated harfbuzz to 7.0.1 --- 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 bef8afa9d..35980f19c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.0.zip", - "filename": "harfbuzz-7.0.0.zip", - "dir": "harfbuzz-7.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", + "filename": "harfbuzz-7.0.1.zip", + "dir": "harfbuzz-7.0.1", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 36bcc0a89866bf9ca3f79c61470d9ab4a58d74ec Mon Sep 17 00:00:00 2001 From: Jasper van der Neut Date: Tue, 21 Feb 2023 10:34:41 +0100 Subject: [PATCH 067/155] Support saving PDF with different X and Y resolution. Add a `dpi` parameter to the PDF save function, which accepts a tuple with X and Y dpi. This is useful for converting tiffg3 (fax) images to pdf, which have split dpi like (204,391), (204,196) or (204,98). --- Tests/test_file_pdf.py | 42 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 5 ++++ src/PIL/PdfImagePlugin.py | 19 ++++++++----- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 216b93ca9..a45b22bf6 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,6 +80,48 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) +def test_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + +def test_resolution_and_dpi(tmp_path): + im = hopper() + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, resolution=200, dpi=(75, 150)) + + with open(outfile, "rb") as fp: + contents = fp.read() + + size = tuple( + float(d) + for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") + ) + assert size == (122.88, 61.44) + + size = tuple( + float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert size == (122.88, 61.44) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..081b84963 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1497,6 +1497,11 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum image, will determine the physical dimensions of the page that will be saved in the PDF. +**dpi** + A tuple of (x_resolution, y_resolution), with inches as the resolution + unit. If both the ``resolution`` parameter and the ``dpi`` parameter are + present, ``resolution`` will be ignored. + **title** The document’s title. If not appending to an existing PDF file, this will default to the filename. diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index baad4939f..d4f1ef93a 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,7 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - resolution = im.encoderinfo.get("resolution", 72.0) + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) + + dpi = im.encoderinfo.get("dpi") + if dpi: + x_resolution = dpi[0] + y_resolution = dpi[1] info = { "title": None @@ -214,8 +219,8 @@ def _save(im, fp, filename, save_all=False): stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / resolution, - Height=height, # * 72.0 / resolution, + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, Filter=filter, BitsPerComponent=bits, Decode=decode, @@ -235,8 +240,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -245,8 +250,8 @@ def _save(im, fp, filename, save_all=False): # page contents page_contents = b"q %f 0 0 %f 0 0 cm /image Do Q\n" % ( - width * 72.0 / resolution, - height * 72.0 / resolution, + width * 72.0 / x_resolution, + height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From be489287d2d8923ad57695f1bdf0d28db8be5757 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Feb 2023 18:59:51 +1100 Subject: [PATCH 068/155] Parametrized test --- Tests/test_file_pdf.py | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index a45b22bf6..2afec9960 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -80,32 +80,18 @@ def test_resolution(tmp_path): assert size == (61.44, 61.44) -def test_dpi(tmp_path): +@pytest.mark.parametrize( + "params", + ( + {"dpi": (75, 150)}, + {"dpi": (75, 150), "resolution": 200}, + ), +) +def test_dpi(params, tmp_path): im = hopper() outfile = str(tmp_path / "temp.pdf") - im.save(outfile, dpi=(75, 150)) - - with open(outfile, "rb") as fp: - contents = fp.read() - - size = tuple( - float(d) - for d in contents.split(b"stream\nq ")[1].split(b" 0 0 cm")[0].split(b" 0 0 ") - ) - assert size == (122.88, 61.44) - - size = tuple( - float(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert size == (122.88, 61.44) - - -def test_resolution_and_dpi(tmp_path): - im = hopper() - - outfile = str(tmp_path / "temp.pdf") - im.save(outfile, resolution=200, dpi=(75, 150)) + im.save(outfile, **params) with open(outfile, "rb") as fp: contents = fp.read() From 0d667f5e0b756844e22dbb78b71c84279992ca2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 21:12:11 +1100 Subject: [PATCH 069/155] Do not read "resolution" parameter if it will not be used --- src/PIL/PdfImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index d4f1ef93a..4fa1998ba 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -53,12 +53,12 @@ def _save(im, fp, filename, save_all=False): else: existing_pdf = PdfParser.PdfParser(f=fp, filename=filename, mode="w+b") - x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) - dpi = im.encoderinfo.get("dpi") if dpi: x_resolution = dpi[0] y_resolution = dpi[1] + else: + x_resolution = y_resolution = im.encoderinfo.get("resolution", 72.0) info = { "title": None From 21d13e1dea032d734572a9bb954c827aba2b29d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Feb 2023 23:22:18 +1100 Subject: [PATCH 070/155] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 34609314c..f36ad5d46 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.3) + assert_image_similar(result, expected.convert("RGB"), 0.5) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 5c8a9165abcf7c2d2ec2829681e88726dafddaf4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 23 Feb 2023 15:18:11 +0200 Subject: [PATCH 071/155] Fix up pytest.raises lambda: uses --- Tests/test_imagefont.py | 37 ++++++++++++++-------------- Tests/test_imagefontctl.py | 49 +++++++++++++------------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 306a2f1bf..b115517ac 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -351,7 +351,8 @@ def test_rotated_transposed_font(font, orientation): assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] # text length is undefined for vertical text - pytest.raises(ValueError, draw.textlength, word) + with pytest.raises(ValueError): + draw.textlength(word) @pytest.mark.parametrize( @@ -872,25 +873,23 @@ def test_anchor_invalid(font): d.font = font for anchor in ["", "l", "a", "lax", "sa", "xa", "lx"]: - pytest.raises(ValueError, lambda: font.getmask2("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: font.getbbox("hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.text((0, 0), "hello", anchor=anchor)) - pytest.raises(ValueError, lambda: d.textbbox((0, 0), "hello", anchor=anchor)) - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor) + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor) + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) for anchor in ["lt", "lb"]: - pytest.raises( - ValueError, lambda: d.multiline_text((0, 0), "foo\nbar", anchor=anchor) - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor) + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor) @pytest.mark.parametrize("bpp", (1, 2, 4, 8)) diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index cf039e86e..6099b04e4 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -360,37 +360,20 @@ def test_anchor_invalid_ttb(): d.font = font for anchor in ["", "l", "a", "lax", "xa", "la", "ls", "ld", "lx"]: - pytest.raises( - ValueError, lambda: font.getmask2("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: font.getbbox("hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, lambda: d.text((0, 0), "hello", anchor=anchor, direction="ttb") - ) - pytest.raises( - ValueError, - lambda: d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_text( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox( - (0, 0), "foo\nbar", anchor=anchor, direction="ttb" - ), - ) + with pytest.raises(ValueError): + font.getmask2("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + font.getbbox("hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.text((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.textbbox((0, 0), "hello", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor=anchor, direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor=anchor, direction="ttb") # ttb multiline text does not support anchors at all - pytest.raises( - ValueError, - lambda: d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) - pytest.raises( - ValueError, - lambda: d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb"), - ) + with pytest.raises(ValueError): + d.multiline_text((0, 0), "foo\nbar", anchor="mm", direction="ttb") + with pytest.raises(ValueError): + d.multiline_textbbox((0, 0), "foo\nbar", anchor="mm", direction="ttb") From 43128ce7165528a8f2bb168e0c89369bbb61817c Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 23 Feb 2023 15:30:38 +0200 Subject: [PATCH 072/155] Fix up pytest.warns lambda: uses --- Tests/test_core_resources.py | 21 +++++++++++---------- Tests/test_decompression_bomb.py | 10 +++++----- Tests/test_file_apng.py | 17 ++++------------- Tests/test_file_dcx.py | 5 ++--- Tests/test_file_fli.py | 5 ++--- Tests/test_file_gif.py | 8 ++++---- Tests/test_file_ico.py | 4 +--- Tests/test_file_im.py | 5 ++--- Tests/test_file_mpo.py | 5 ++--- Tests/test_file_psd.py | 5 ++--- Tests/test_file_spider.py | 5 ++--- Tests/test_file_tar.py | 4 +--- Tests/test_file_tga.py | 4 +++- Tests/test_file_tiff.py | 8 ++++---- Tests/test_file_tiff_metadata.py | 6 ++++-- Tests/test_file_webp.py | 4 +++- 16 files changed, 52 insertions(+), 64 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 385192a3c..e4c0001d1 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -177,13 +177,14 @@ class TestEnvVars: Image._apply_env_variables({"PILLOW_BLOCK_SIZE": "2m"}) assert Image.core.get_block_size() == 2 * 1024 * 1024 - def test_warnings(self): - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_ALIGNMENT": "15"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCK_SIZE": "1024"} - ) - pytest.warns( - UserWarning, Image._apply_env_variables, {"PILLOW_BLOCKS_MAX": "wat"} - ) + @pytest.mark.parametrize( + "vars", + [ + {"PILLOW_ALIGNMENT": "15"}, + {"PILLOW_BLOCK_SIZE": "1024"}, + {"PILLOW_BLOCKS_MAX": "wat"}, + ], + ) + def test_warnings(self, vars): + with pytest.warns(UserWarning): + Image._apply_env_variables(vars) diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 63071b78c..4fd02449c 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -36,12 +36,10 @@ class TestDecompressionBomb: Image.MAX_IMAGE_PIXELS = 128 * 128 - 1 assert Image.MAX_IMAGE_PIXELS == 128 * 128 - 1 - def open(): + with pytest.warns(Image.DecompressionBombWarning): with Image.open(TEST_FILE): pass - pytest.warns(Image.DecompressionBombWarning, open) - def test_exception(self): # Set limit to trigger exception on the test file Image.MAX_IMAGE_PIXELS = 64 * 128 - 1 @@ -87,7 +85,8 @@ class TestDecompressionCrop: # same decompression bomb warnings on them. with hopper() as src: box = (0, 0, src.width * 2, src.height * 2) - pytest.warns(Image.DecompressionBombWarning, src.crop, box) + with pytest.warns(Image.DecompressionBombWarning): + src.crop(box) def test_crop_decompression_checks(self): im = Image.new("RGB", (100, 100)) @@ -95,7 +94,8 @@ class TestDecompressionCrop: for value in ((-9999, -9999, -9990, -9990), (-999, -999, -990, -990)): assert im.crop(value).size == (9, 9) - pytest.warns(Image.DecompressionBombWarning, im.crop, (-160, -160, 99, 99)) + with pytest.warns(Image.DecompressionBombWarning): + im.crop((-160, -160, 99, 99)) with pytest.raises(Image.DecompressionBombError): im.crop((-99909, -99990, 99999, 99999)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 51637c786..9f850d0e9 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -263,12 +263,9 @@ def test_apng_chunk_errors(): with Image.open("Tests/images/apng/chunk_no_actl.png") as im: assert not im.is_animated - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() - assert not im.is_animated - - pytest.warns(UserWarning, open) with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated @@ -287,21 +284,17 @@ def test_apng_chunk_errors(): def test_apng_syntax_errors(): - def open_frames_zero(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: assert not im.is_animated with pytest.raises(OSError): im.load() - pytest.warns(UserWarning, open_frames_zero) - - def open_frames_zero_default(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open_frames_zero_default) - # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: @@ -316,13 +309,11 @@ def test_apng_syntax_errors(): im.seek(im.n_frames - 1) im.load() - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: assert not im.is_animated im.load() - pytest.warns(UserWarning, open) - @pytest.mark.parametrize( "test_file", diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index ef378b24a..1adda7729 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -24,11 +24,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_FILE) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 70d4d76db..cb767a0d8 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -32,11 +32,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(static_test_file) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index bce72d192..8f11f0a1c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -32,11 +32,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_GIF) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): @@ -1087,7 +1086,8 @@ def test_rgb_transparency(tmp_path): im = Image.new("RGB", (1, 1)) im.info["transparency"] = b"" ims = [Image.new("RGB", (1, 1))] - pytest.warns(UserWarning, im.save, out, save_all=True, append_images=ims) + with pytest.warns(UserWarning): + im.save(out, save_all=True, append_images=ims) with Image.open(out) as reloaded: assert "transparency" not in reloaded.info diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 9c1c3cf17..4e6dbe6ed 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -212,12 +212,10 @@ def test_save_append_images(tmp_path): def test_unexpected_size(): # This image has been manually hexedited to state that it is 16x32 # while the image within is still 16x16 - def open(): + with pytest.warns(UserWarning): with Image.open("Tests/images/hopper_unexpected.ico") as im: assert im.size == (16, 16) - pytest.warns(UserWarning, open) - def test_draw_reloaded(tmp_path): with Image.open(TEST_ICO_FILE) as im: diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 425e690d6..bdc704ee1 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -28,11 +28,10 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_IM) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index f0dedc2de..ea701f70d 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -38,11 +38,10 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(test_files[0]) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 036cb9d4b..ff78993fe 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -23,11 +23,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(test_file) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 011e208d8..122690e34 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -21,11 +21,10 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): im = Image.open(TEST_FILE) im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(): diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 799c243d6..b27fa25f3 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -29,11 +29,9 @@ def test_sanity(codec, test_path, format): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - def open(): + with pytest.warns(ResourceWarning): TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") - pytest.warns(ResourceWarning, open) - def test_close(): with warnings.catch_warnings(): diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index bac00e855..1a5730f49 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -163,7 +163,9 @@ def test_save_id_section(tmp_path): # Save with custom id section greater than 255 characters id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with pytest.warns(UserWarning): + im.save(out, id_section=id_section) + with Image.open(out) as test_im: assert test_im.info["id_section"] == id_section[:255] diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 70142747c..d0d9ed891 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -57,11 +57,10 @@ class TestFileTiff: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self): - def open(): + with pytest.warns(ResourceWarning): im = Image.open("Tests/images/multipage.tiff") im.load() - - pytest.warns(ResourceWarning, open) + del im def test_closed_file(self): with warnings.catch_warnings(): @@ -231,7 +230,8 @@ class TestFileTiff: def test_bad_exif(self): with Image.open("Tests/images/hopper_bad_exif.jpg") as i: # Should not raise struct.error. - pytest.warns(UserWarning, i._getexif) + with pytest.warns(UserWarning): + i._getexif() def test_save_rgba(self, tmp_path): im = hopper("RGBA") diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index fdabae3a3..9a5681526 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -252,7 +252,8 @@ def test_empty_metadata(): head = f.read(8) info = TiffImagePlugin.ImageFileDirectory(head) # Should not raise struct.error. - pytest.warns(UserWarning, info.load, f) + with pytest.warns(UserWarning): + info.load(f) def test_iccprofile(tmp_path): @@ -422,7 +423,8 @@ def test_too_many_entries(): ifd.tagtype[277] = TiffTags.SHORT # Should not raise ValueError. - pytest.warns(UserWarning, lambda: ifd[277]) + with pytest.warns(UserWarning): + _ = ifd[277] def test_tag_group_data(): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f1bdc59cf..335201fe1 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -29,7 +29,9 @@ class TestUnsupportedWebp: WebPImagePlugin.SUPPORTED = False file_path = "Tests/images/hopper.webp" - pytest.warns(UserWarning, lambda: pytest.raises(OSError, Image.open, file_path)) + with pytest.warns(UserWarning): + with pytest.raises(OSError): + Image.open(file_path) if HAVE_WEBP: WebPImagePlugin.SUPPORTED = True From acc7e0b469ac973bf1ab79bf8369a8b13769d169 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:00:24 +1100 Subject: [PATCH 073/155] Highlight code example --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 926a2f75a..9a3ddcab7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1104,7 +1104,7 @@ using the general tags available through tiffinfo. Either an integer or a float. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. For consistency with other image formats, the x and y resolutions of the dpi will be rounded to the nearest integer. From 742aff3718cebdad390ad808e3cae181ea749e27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:17:10 +1100 Subject: [PATCH 074/155] Replace Python code-blocks with double colons --- docs/handbook/image-file-formats.rst | 4 +-- docs/handbook/text-anchors.rst | 2 +- docs/reference/Image.rst | 12 ++----- docs/reference/ImageDraw.rst | 18 ++++------ docs/reference/ImageEnhance.rst | 2 +- docs/reference/ImageFile.rst | 2 +- docs/reference/ImageFilter.rst | 2 +- docs/reference/ImageFont.rst | 2 +- docs/reference/ImageMath.rst | 2 +- docs/reference/ImageSequence.rst | 2 +- docs/reference/PixelAccess.rst | 8 ++--- docs/reference/PyAccess.rst | 8 ++--- docs/releasenotes/6.2.0.rst | 8 ++--- docs/releasenotes/7.1.0.rst | 4 +-- docs/releasenotes/8.2.0.rst | 4 +-- docs/releasenotes/8.4.0.rst | 4 +-- docs/releasenotes/9.1.0.rst | 8 ++--- src/PIL/ImageChops.py | 52 +++++++--------------------- src/PIL/ImageFont.py | 12 ++----- 19 files changed, 44 insertions(+), 112 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index d6b42589a..4c2af3db8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1402,9 +1402,7 @@ at 72 dpi. To load it at another resolution:: To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF -handler. - -.. code-block:: python +handler. :: from PIL import Image from PIL import WmfImagePlugin diff --git a/docs/handbook/text-anchors.rst b/docs/handbook/text-anchors.rst index 0aecd3483..3a9572ab2 100644 --- a/docs/handbook/text-anchors.rst +++ b/docs/handbook/text-anchors.rst @@ -29,7 +29,7 @@ For example, in the following image, the text is ``ms`` (middle-baseline) aligne :alt: ms (middle-baseline) aligned text. :align: left -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 976b148fc..0eba1141a 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -17,9 +17,7 @@ Open, rotate, and display an image (using the default viewer) The following script loads an image, rotates it 45 degrees, and displays it using an external viewer (usually xv on Unix, and the Paint program on -Windows). - -.. code-block:: python +Windows). :: from PIL import Image with Image.open("hopper.jpg") as im: @@ -29,9 +27,7 @@ Create thumbnails ^^^^^^^^^^^^^^^^^ The following script creates nice thumbnails of all JPEG images in the -current directory preserving aspect ratios with 128x128 max resolution. - -.. code-block:: python +current directory preserving aspect ratios with 128x128 max resolution. :: from PIL import Image import glob, os @@ -242,9 +238,7 @@ This rotates the input image by ``theta`` degrees counter clockwise:: .. automethod:: PIL.Image.Image.transpose This flips the input image by using the :data:`Transpose.FLIP_LEFT_RIGHT` -method. - -.. code-block:: python +method. :: from PIL import Image diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9aa26916a..e325a0280 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -16,7 +16,7 @@ For a more advanced drawing library for PIL, see the `aggdraw module`_. Example: Draw a gray cross over an image ---------------------------------------- -.. code-block:: python +:: import sys from PIL import Image, ImageDraw @@ -78,7 +78,7 @@ libraries, and may not available in all PIL builds. Example: Draw Partial Opacity Text ---------------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -105,7 +105,7 @@ Example: Draw Partial Opacity Text Example: Draw Multiline Text ---------------------------- -.. code-block:: python +:: from PIL import Image, ImageDraw, ImageFont @@ -597,18 +597,14 @@ Methods string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = draw.textlength("Hello", font) world = draw.textlength("World", font) hello_world = hello + world # not adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # may fail - use - - .. code-block:: python + use :: hello = draw.textlength("HelloW", font) - draw.textlength( "W", font @@ -617,9 +613,7 @@ Methods hello_world = hello + world # adjusted for kerning assert hello_world == draw.textlength("HelloWorld", font) # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 29ceee314..b27228ec9 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -10,7 +10,7 @@ for image enhancement. Example: Vary the sharpness of an image --------------------------------------- -.. code-block:: python +:: from PIL import ImageEnhance diff --git a/docs/reference/ImageFile.rst b/docs/reference/ImageFile.rst index 3cf59c610..047990f1c 100644 --- a/docs/reference/ImageFile.rst +++ b/docs/reference/ImageFile.rst @@ -15,7 +15,7 @@ and **xmllib** modules. Example: Parse an image ----------------------- -.. code-block:: python +:: from PIL import ImageFile diff --git a/docs/reference/ImageFilter.rst b/docs/reference/ImageFilter.rst index c85da4fb5..044aede62 100644 --- a/docs/reference/ImageFilter.rst +++ b/docs/reference/ImageFilter.rst @@ -11,7 +11,7 @@ filters, which can be be used with the :py:meth:`Image.filter() Example: Filter an image ------------------------ -.. code-block:: python +:: from PIL import ImageFilter diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 516fa63a7..946bd3c4b 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -21,7 +21,7 @@ the imToolkit package. Example ------- -.. code-block:: python +:: from PIL import ImageFont, ImageDraw diff --git a/docs/reference/ImageMath.rst b/docs/reference/ImageMath.rst index 63f88fddd..118d988d6 100644 --- a/docs/reference/ImageMath.rst +++ b/docs/reference/ImageMath.rst @@ -11,7 +11,7 @@ an expression string and one or more images. Example: Using the :py:mod:`~PIL.ImageMath` module -------------------------------------------------- -.. code-block:: python +:: from PIL import Image, ImageMath diff --git a/docs/reference/ImageSequence.rst b/docs/reference/ImageSequence.rst index f2e7d9edd..a27b2fb4e 100644 --- a/docs/reference/ImageSequence.rst +++ b/docs/reference/ImageSequence.rst @@ -10,7 +10,7 @@ iterate over the frames of an image sequence. Extracting frames from an animation ----------------------------------- -.. code-block:: python +:: from PIL import Image, ImageSequence diff --git a/docs/reference/PixelAccess.rst b/docs/reference/PixelAccess.rst index b234b7b4e..04d6f5dcd 100644 --- a/docs/reference/PixelAccess.rst +++ b/docs/reference/PixelAccess.rst @@ -18,9 +18,7 @@ Example ------- The following script loads an image, accesses one pixel from it, then -changes it. - -.. code-block:: python +changes it. :: from PIL import Image @@ -35,9 +33,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/reference/PyAccess.rst b/docs/reference/PyAccess.rst index f9eb9b524..ed58ca3a5 100644 --- a/docs/reference/PyAccess.rst +++ b/docs/reference/PyAccess.rst @@ -17,9 +17,7 @@ The :py:mod:`~PIL.PyAccess` module provides a CFFI/Python implementation of the Example ------- -The following script loads an image, accesses one pixel from it, then changes it. - -.. code-block:: python +The following script loads an image, accesses one pixel from it, then changes it. :: from PIL import Image @@ -34,9 +32,7 @@ Results in the following:: (23, 24, 68) (0, 0, 0) -Access using negative indexes is also possible. - -.. code-block:: python +Access using negative indexes is also possible. :: px[-1, -1] = (0, 0, 0) print(px[-1, -1]) diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 20a009cc1..0fb33de75 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -10,9 +10,7 @@ Text stroking ``stroke_width`` and ``stroke_fill`` arguments have been added to text drawing operations. They allow text to be outlined, setting the width of the stroke and and the color respectively. If not provided, ``stroke_fill`` will default to -the ``fill`` parameter. - -.. code-block:: python +the ``fill`` parameter. :: from PIL import Image, ImageDraw, ImageFont @@ -28,9 +26,7 @@ the ``fill`` parameter. draw.multiline_text((10, 10), "A\nB", "#f00", font, stroke_width=2, stroke_fill="#0f0") -For example, - -.. code-block:: python +For example, :: from PIL import Image, ImageDraw, ImageFont diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index 0024a537d..cb46f127c 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -10,9 +10,7 @@ Allow saving of zero quality JPEG images If no quality was specified when saving a JPEG, Pillow internally used a value of zero to indicate that the default quality should be used. However, this removed the ability to actually save a JPEG with zero quality. This has now -been resolved. - -.. code-block:: python +been resolved. :: from PIL import Image im = Image.open("hopper.jpg") diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index c902ccf71..f11953168 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -76,9 +76,7 @@ ImageDraw.rounded_rectangle Added :py:meth:`~PIL.ImageDraw.ImageDraw.rounded_rectangle`. It works the same as :py:meth:`~PIL.ImageDraw.ImageDraw.rectangle`, except with an additional ``radius`` argument. ``radius`` is limited to half of the width or the height, so that users can -create a circle, but not any other ellipse. - -.. code-block:: python +create a circle, but not any other ellipse. :: from PIL import Image, ImageDraw im = Image.new("RGB", (200, 200)) diff --git a/docs/releasenotes/8.4.0.rst b/docs/releasenotes/8.4.0.rst index 9becf9146..e61471e72 100644 --- a/docs/releasenotes/8.4.0.rst +++ b/docs/releasenotes/8.4.0.rst @@ -24,9 +24,7 @@ Added "transparency" argument for loading EPS images This new argument switches the Ghostscript device from "ppmraw" to "pngalpha", generating an RGBA image with a transparent background instead of an RGB image with a -white background. - -.. code-block:: python +white background. :: with Image.open("sample.eps") as im: im.load(transparency=True) diff --git a/docs/releasenotes/9.1.0.rst b/docs/releasenotes/9.1.0.rst index e97b58a41..19690ca59 100644 --- a/docs/releasenotes/9.1.0.rst +++ b/docs/releasenotes/9.1.0.rst @@ -182,17 +182,13 @@ GifImagePlugin loading strategy Pillow 9.0.0 introduced the conversion of subsequent GIF frames to ``RGB`` or ``RGBA``. This behaviour can now be changed so that the first ``P`` frame is converted to ``RGB`` as -well. - -.. code-block:: python +well. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS Or subsequent frames can be kept in ``P`` mode as long as there is only a single -palette. - -.. code-block:: python +palette. :: from PIL import GifImagePlugin GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY diff --git a/src/PIL/ImageChops.py b/src/PIL/ImageChops.py index fec4694b2..701200317 100644 --- a/src/PIL/ImageChops.py +++ b/src/PIL/ImageChops.py @@ -38,9 +38,7 @@ def duplicate(image): def invert(image): """ - Invert an image (channel). - - .. code-block:: python + Invert an image (channel). :: out = MAX - image @@ -54,9 +52,7 @@ def invert(image): def lighter(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the lighter values. - - .. code-block:: python + the lighter values. :: out = max(image1, image2) @@ -71,9 +67,7 @@ def lighter(image1, image2): def darker(image1, image2): """ Compares the two images, pixel by pixel, and returns a new image containing - the darker values. - - .. code-block:: python + the darker values. :: out = min(image1, image2) @@ -88,9 +82,7 @@ def darker(image1, image2): def difference(image1, image2): """ Returns the absolute value of the pixel-by-pixel difference between the two - images. - - .. code-block:: python + images. :: out = abs(image1 - image2) @@ -107,9 +99,7 @@ def multiply(image1, image2): Superimposes two images on top of each other. If you multiply an image with a solid black image, the result is black. If - you multiply with a solid white image, the image is unaffected. - - .. code-block:: python + you multiply with a solid white image, the image is unaffected. :: out = image1 * image2 / MAX @@ -123,9 +113,7 @@ def multiply(image1, image2): def screen(image1, image2): """ - Superimposes two inverted images on top of each other. - - .. code-block:: python + Superimposes two inverted images on top of each other. :: out = MAX - ((MAX - image1) * (MAX - image2) / MAX) @@ -176,9 +164,7 @@ def overlay(image1, image2): def add(image1, image2, scale=1.0, offset=0): """ Adds two images, dividing the result by scale and adding the - offset. If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + offset. If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 + image2) / scale + offset) @@ -193,9 +179,7 @@ def add(image1, image2, scale=1.0, offset=0): def subtract(image1, image2, scale=1.0, offset=0): """ Subtracts two images, dividing the result by scale and adding the offset. - If omitted, scale defaults to 1.0, and offset to 0.0. - - .. code-block:: python + If omitted, scale defaults to 1.0, and offset to 0.0. :: out = ((image1 - image2) / scale + offset) @@ -208,9 +192,7 @@ def subtract(image1, image2, scale=1.0, offset=0): def add_modulo(image1, image2): - """Add two images, without clipping the result. - - .. code-block:: python + """Add two images, without clipping the result. :: out = ((image1 + image2) % MAX) @@ -223,9 +205,7 @@ def add_modulo(image1, image2): def subtract_modulo(image1, image2): - """Subtract two images, without clipping the result. - - .. code-block:: python + """Subtract two images, without clipping the result. :: out = ((image1 - image2) % MAX) @@ -243,9 +223,7 @@ def logical_and(image1, image2): Both of the images must have mode "1". If you would like to perform a logical AND on an image with a mode other than "1", try :py:meth:`~PIL.ImageChops.multiply` instead, using a black-and-white mask - as the second image. - - .. code-block:: python + as the second image. :: out = ((image1 and image2) % MAX) @@ -260,9 +238,7 @@ def logical_and(image1, image2): def logical_or(image1, image2): """Logical OR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((image1 or image2) % MAX) @@ -277,9 +253,7 @@ def logical_or(image1, image2): def logical_xor(image1, image2): """Logical XOR between two images. - Both of the images must have mode "1". - - .. code-block:: python + Both of the images must have mode "1". :: out = ((bool(image1) != bool(image2)) % MAX) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index bd13c391e..173b2926f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -297,27 +297,21 @@ class FreeTypeFont: string due to kerning. If you need to adjust for kerning, include the following character and subtract its length. - For example, instead of - - .. code-block:: python + For example, instead of :: hello = font.getlength("Hello") world = font.getlength("World") hello_world = hello + world # not adjusted for kerning assert hello_world == font.getlength("HelloWorld") # may fail - use - - .. code-block:: python + use :: hello = font.getlength("HelloW") - font.getlength("W") # adjusted for kerning world = font.getlength("World") hello_world = hello + world # adjusted for kerning assert hello_world == font.getlength("HelloWorld") # True - or disable kerning with (requires libraqm) - - .. code-block:: python + or disable kerning with (requires libraqm) :: hello = draw.textlength("Hello", font, features=["-kern"]) world = draw.textlength("World", font, features=["-kern"]) From 19299acbb6688d2aaf33e53f64c9cbb04545e274 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:39:48 +1100 Subject: [PATCH 075/155] Relax roundtrip check --- Tests/test_qt_image_qapplication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index f36ad5d46..4929fa933 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -48,7 +48,7 @@ if ImageQt.qt_is_installed: def roundtrip(expected): result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) # Qt saves all pixmaps as rgb - assert_image_similar(result, expected.convert("RGB"), 0.5) + assert_image_similar(result, expected.convert("RGB"), 1) @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") From 3c3d88845072b0b8b8a9a00787fb03d733def3b7 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Thu, 23 Feb 2023 23:19:13 +0100 Subject: [PATCH 076/155] Update docs/handbook/image-file-formats.rst Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 081b84963..597d1b644 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1498,7 +1498,7 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum saved in the PDF. **dpi** - A tuple of (x_resolution, y_resolution), with inches as the resolution + A tuple of ``(x_resolution, y_resolution)``, with inches as the resolution unit. If both the ``resolution`` parameter and the ``dpi`` parameter are present, ``resolution`` will be ignored. From 57acab55cbf0f00f3064d11ccfd3843c55cdceb0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 12:55:13 +1100 Subject: [PATCH 077/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index aa03cfc40..d37ba4ab3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support saving PDF with different X and Y resolutions #6961 + [jvanderneutstulen, radarhere, hugovk] + - Fixed writing int as UNDEFINED tag #6950 [radarhere] From 44c4e67fe13719672219aa2de67b8c19f921c191 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:26:18 +0200 Subject: [PATCH 078/155] Fix `vars` to `var` Co-authored-by: Hugo van Kemenade --- Tests/test_core_resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index e4c0001d1..cb6cde8eb 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -178,13 +178,13 @@ class TestEnvVars: assert Image.core.get_block_size() == 2 * 1024 * 1024 @pytest.mark.parametrize( - "vars", + "var", [ {"PILLOW_ALIGNMENT": "15"}, {"PILLOW_BLOCK_SIZE": "1024"}, {"PILLOW_BLOCKS_MAX": "wat"}, ], ) - def test_warnings(self, vars): + def test_warnings(self, var): with pytest.warns(UserWarning): - Image._apply_env_variables(vars) + Image._apply_env_variables(var) From f52bbf895036f25932a479a46b20b07f2eb0c1de Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:58:51 +0200 Subject: [PATCH 079/155] Clarify variable names in BdfFontFile Co-authored-by: Yay295 --- src/PIL/BdfFontFile.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index e0dd4dede..3f7b760d6 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,25 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - [x, y, l, d] = [int(p) for p in props["BBX"].split()] - [dx, dy] = [int(p) for p in props["DWIDTH"].split()] + # The word BBX followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBox, BBoy) of the lower left corner + # from the origin of the character. + width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y) + # The word DWIDTH followed by the width in x and y of the character in device units. + dx, dy = [int(p) for p in props["DWIDTH"].split()] + + bbox = ( + (dx, dy), + (x_disp, -y_disp - height, width + x_disp, -y_disp), + (0, 0, width, height), + ) try: - im = Image.frombytes("1", (x, y), bitmap, "hex", "1") + im = Image.frombytes("1", (width, height), bitmap, "hex", "1") except ValueError: # deal with zero-width characters - im = Image.new("1", (x, y)) + im = Image.new("1", (width, height)) return id, int(props["ENCODING"]), bbox, im From b6b72170a8d81255e6f491e657e0614102a0e347 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:59:54 +0200 Subject: [PATCH 080/155] Clarify variable names in Image Co-authored-by: Yay295 --- src/PIL/Image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 63bad83a1..670907c67 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -767,12 +767,12 @@ class Image: data = [] while True: - l, s, d = e.encode(bufsize) - data.append(d) - if s: + length, error_code, chunk = e.encode(bufsize) + data.append(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} in tobytes" + if error_code < 0: + msg = f"encoder error {error_code} in tobytes" raise RuntimeError(msg) return b"".join(data) From 04be46d484eccb633b1ad8058e1c4f39208589b0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:04:38 +0200 Subject: [PATCH 081/155] Clarify variable names in ImageFile Co-authored-by: Yay295 --- src/PIL/ImageFile.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 132490a8e..dfa715686 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - l, s = encoder.encode_to_pyfd() + length, error_code = encoder.encode_to_pyfd() else: if exc: # compress to Python file-compatible object while True: - l, s, d = encoder.encode(bufsize) - fp.write(d) - if s: + length, error_code, chunk = encoder.encode(bufsize) + fp.write(chunk) + if error_code: break else: # slight speedup: compress to real file object - s = encoder.encode_to_file(fh, bufsize) - if s < 0: - msg = f"encoder error {s} when writing image file" + error_code = encoder.encode_to_file(fh, bufsize) + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() From 6f79e653d68c7bae9c0303f8d8ddd68a3a1cb3f0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:07:29 +0200 Subject: [PATCH 082/155] Clarify variable names in PcfFontFile Co-authored-by: Yay295 --- src/PIL/PcfFontFile.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index d5f510f03..1a4b8f6d9 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,9 +86,23 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - x, y, l, r, w, a, d, f = metrics[ix] - glyph = (w, 0), (l, d - y, x + l, d), (0, 0, x, y), bitmaps[ix] - self.glyph[ch] = glyph + ix_metrics = metrics[ix] + ( + xsize, + ysize, + left, + right, + width, + ascent, + descent, + attributes, + ) = ix_metrics + self.glyph[ch] = ( + (width, 0), + (left, descent - ysize, xsize + left, descent), + (0, 0, xsize, ysize), + bitmaps[ix], + ) def _getformat(self, tag): format, size, offset = self.toc[tag] @@ -206,7 +220,7 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y, l, r, w, a, d, f = metrics[i] + x, y = metrics[i][0], metrics[i][1] b, e = offsets[i], offsets[i + 1] bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) From 8e18415cc54c1c87183d36a184b5360d77fb8571 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 10:09:14 +0200 Subject: [PATCH 083/155] Clarify variable names in TiffImagePlugin Co-authored-by: Yay295 --- src/PIL/TiffImagePlugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index aaaf8fcb9..0491a736d 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - l, s, d = e.encode(16 * 1024) + length, error_code, chunk = e.encode(16 * 1024) if not _fp: - fp.write(d) - if s: + fp.write(chunk) + if error_code: break - if s < 0: - msg = f"encoder error {s} when writing image file" + if error_code < 0: + msg = f"encoder error {error_code} when writing image file" raise OSError(msg) else: From 1263018d2afbc19e9dce8b82d064cccc2e5ccfca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 23:00:29 +1100 Subject: [PATCH 084/155] Assert value instead of assigning unused variable --- Tests/test_file_tiff_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 9a5681526..b7d100e7a 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -419,12 +419,12 @@ def test_too_many_entries(): ifd = TiffImagePlugin.ImageFileDirectory_v2() # 277: ("SamplesPerPixel", SHORT, 1), - ifd._tagdata[277] = struct.pack("hh", 4, 4) + ifd._tagdata[277] = struct.pack(" Date: Sat, 25 Feb 2023 16:44:07 +1100 Subject: [PATCH 085/155] Allow comments in FITS images --- Tests/test_file_fits.py | 6 ++++++ src/PIL/FitsImagePlugin.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index d2f5a6d17..3048827e0 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -44,6 +44,12 @@ def test_naxis_zero(): pass +def test_comment(): + image_data = b"SIMPLE = T / comment string" + with pytest.raises(OSError): + FitsImagePlugin.FitsImageFile(BytesIO(image_data)) + + def test_stub_deprecated(): class Handler: opened = False diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1185ef2d3..1359aeb12 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -32,7 +32,7 @@ class FitsImageFile(ImageFile.ImageFile): keyword = header[:8].strip() if keyword == b"END": break - value = header[8:].strip() + value = header[8:].split(b"/")[0].strip() if value.startswith(b"="): value = value[1:].strip() if not headers and (not _accept(keyword) or value != b"T"): From dbcd7372e554ee420a156b968eb9a609ec66ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 18:46:07 +1100 Subject: [PATCH 086/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d37ba4ab3..15decc7f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow comments in FITS images #6973 + [radarhere] + - Support saving PDF with different X and Y resolutions #6961 [jvanderneutstulen, radarhere, hugovk] From 132fb9360b291eafedd3ea8238ceebd93a094e87 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 19:10:47 +1100 Subject: [PATCH 087/155] Added memoryview support to frombytes() --- Tests/test_image_frombytes.py | 11 +++++++++-- src/decode.c | 7 +++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 7fb05cda7..c299e4544 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -1,10 +1,17 @@ +import pytest + from PIL import Image from .helper import assert_image_equal, hopper -def test_sanity(): +@pytest.mark.parametrize("data_type", ("bytes", "memoryview")) +def test_sanity(data_type): im1 = hopper() - im2 = Image.frombytes(im1.mode, im1.size, im1.tobytes()) + + data = im1.tobytes() + if data_type == "memoryview": + data = memoryview(data) + im2 = Image.frombytes(im1.mode, im1.size, data) assert_image_equal(im1, im2) diff --git a/src/decode.c b/src/decode.c index 7a9b956c5..82a3af832 100644 --- a/src/decode.c +++ b/src/decode.c @@ -116,12 +116,11 @@ _dealloc(ImagingDecoderObject *decoder) { static PyObject * _decode(ImagingDecoderObject *decoder, PyObject *args) { - UINT8 *buffer; - Py_ssize_t bufsize; + Py_buffer buffer; int status; ImagingSectionCookie cookie; - if (!PyArg_ParseTuple(args, "y#", &buffer, &bufsize)) { + if (!PyArg_ParseTuple(args, "y*", &buffer)) { return NULL; } @@ -129,7 +128,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionEnter(&cookie); } - status = decoder->decode(decoder->im, &decoder->state, buffer, bufsize); + status = decoder->decode(decoder->im, &decoder->state, buffer.buf, buffer.len); if (!decoder->pulls_fd) { ImagingSectionLeave(&cookie); From 36489c2c396d8801bfa632b6e8d00a094dab5347 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 20:54:35 +1100 Subject: [PATCH 088/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 15decc7f0..fe0230c34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added memoryview support to frombytes() #6974 + [radarhere] + - Allow comments in FITS images #6973 [radarhere] From fcc59a4001bb1b1ad47d1a8c0b0a602336b2dcdd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 13:40:44 +1100 Subject: [PATCH 089/155] Use existing variable names from ImageFile --- src/PIL/Image.py | 14 +++++++------- src/PIL/ImageFile.py | 14 +++++++------- src/PIL/PcfFontFile.py | 11 ++++++----- src/PIL/TiffImagePlugin.py | 10 +++++----- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 670907c67..cf9ab2df6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -765,17 +765,17 @@ class Image: bufsize = max(65536, self.size[0] * 4) # see RawEncode.c - data = [] + output = [] while True: - length, error_code, chunk = e.encode(bufsize) - data.append(chunk) - if error_code: + bytes_consumed, errcode, data = e.encode(bufsize) + output.append(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} in tobytes" + if errcode < 0: + msg = f"encoder error {errcode} in tobytes" raise RuntimeError(msg) - return b"".join(data) + return b"".join(output) def tobitmap(self, name="image"): """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index dfa715686..8e4f7dfb2 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -530,20 +530,20 @@ def _encode_tile(im, fp, tile, bufsize, fh, exc=None): encoder.setimage(im.im, b) if encoder.pushes_fd: encoder.setfd(fp) - length, error_code = encoder.encode_to_pyfd() + errcode = encoder.encode_to_pyfd()[1] else: if exc: # compress to Python file-compatible object while True: - length, error_code, chunk = encoder.encode(bufsize) - fp.write(chunk) - if error_code: + errcode, data = encoder.encode(bufsize)[1:] + fp.write(data) + if errcode: break else: # slight speedup: compress to real file object - error_code = encoder.encode_to_file(fh, bufsize) - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + errcode = encoder.encode_to_file(fh, bufsize) + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) from exc finally: encoder.cleanup() diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 1a4b8f6d9..2300efe40 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -86,7 +86,6 @@ class PcfFontFile(FontFile.FontFile): for ch, ix in enumerate(encoding): if ix is not None: - ix_metrics = metrics[ix] ( xsize, ysize, @@ -96,7 +95,7 @@ class PcfFontFile(FontFile.FontFile): ascent, descent, attributes, - ) = ix_metrics + ) = metrics[ix] self.glyph[ch] = ( (width, 0), (left, descent - ysize, xsize + left, descent), @@ -220,9 +219,11 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - x, y = metrics[i][0], metrics[i][1] - b, e = offsets[i], offsets[i + 1] - bitmaps.append(Image.frombytes("1", (x, y), data[b:e], "raw", mode, pad(x))) + left, right = metrics[i][:2] + b, e = offsets[i : i + 2] + bitmaps.append( + Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + ) return bitmaps diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 0491a736d..04d246dd4 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1845,13 +1845,13 @@ def _save(im, fp, filename): e.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - length, error_code, chunk = e.encode(16 * 1024) + errcode, data = e.encode(16 * 1024)[1:] if not _fp: - fp.write(chunk) - if error_code: + fp.write(data) + if errcode: break - if error_code < 0: - msg = f"encoder error {error_code} when writing image file" + if errcode < 0: + msg = f"encoder error {errcode} when writing image file" raise OSError(msg) else: From c799bd8a03f522dc6cc26f6c974140df60558484 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Feb 2023 14:04:10 +1100 Subject: [PATCH 090/155] Adjusted variable names and comments to better match specification --- src/PIL/BdfFontFile.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/PIL/BdfFontFile.py b/src/PIL/BdfFontFile.py index 3f7b760d6..075d46290 100644 --- a/src/PIL/BdfFontFile.py +++ b/src/PIL/BdfFontFile.py @@ -64,16 +64,18 @@ def bdf_char(f): bitmap.append(s[:-1]) bitmap = b"".join(bitmap) - # The word BBX followed by the width in x (BBw), height in y (BBh), - # and x and y displacement (BBox, BBoy) of the lower left corner - # from the origin of the character. + # The word BBX + # followed by the width in x (BBw), height in y (BBh), + # and x and y displacement (BBxoff0, BByoff0) + # of the lower left corner from the origin of the character. width, height, x_disp, y_disp = [int(p) for p in props["BBX"].split()] - # The word DWIDTH followed by the width in x and y of the character in device units. - dx, dy = [int(p) for p in props["DWIDTH"].split()] + # The word DWIDTH + # followed by the width in x and y of the character in device pixels. + dwx, dwy = [int(p) for p in props["DWIDTH"].split()] bbox = ( - (dx, dy), + (dwx, dwy), (x_disp, -y_disp - height, width + x_disp, -y_disp), (0, 0, width, height), ) From bbbaf3c615e7a60e526e73f3dc6449780dce2271 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Sun, 26 Feb 2023 13:03:29 +0200 Subject: [PATCH 091/155] Update src/PIL/PcfFontFile.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PcfFontFile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 2300efe40..8db5822fe 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -219,10 +219,10 @@ class PcfFontFile(FontFile.FontFile): mode = "1" for i in range(nbitmaps): - left, right = metrics[i][:2] + xsize, ysize = metrics[i][:2] b, e = offsets[i : i + 2] bitmaps.append( - Image.frombytes("1", (left, right), data[b:e], "raw", mode, pad(left)) + Image.frombytes("1", (xsize, ysize), data[b:e], "raw", mode, pad(xsize)) ) return bitmaps From 9c98f4d515036d9bb8094bc5faac6a81eca0b147 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Feb 2023 09:48:41 +1100 Subject: [PATCH 092/155] Release buffer --- src/decode.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/decode.c b/src/decode.c index 82a3af832..7e3fadc04 100644 --- a/src/decode.c +++ b/src/decode.c @@ -134,6 +134,7 @@ _decode(ImagingDecoderObject *decoder, PyObject *args) { ImagingSectionLeave(&cookie); } + PyBuffer_Release(&buffer); return Py_BuildValue("ii", status, decoder->state.errcode); } From 17eadf07fa5315e6d904936fb052e90c49915962 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:49:43 +1100 Subject: [PATCH 093/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index fe0230c34..d5798d41b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added "corners" argument to ImageDraw rounded_rectangle() #6954 + [radarhere] + - Added memoryview support to frombytes() #6974 [radarhere] From 6e9c0ae5a09618afec077a39262d2337dd0a3fee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 28 Feb 2023 20:04:26 +1100 Subject: [PATCH 094/155] Further document that x1 >= x0 and y1 >= y0 --- docs/reference/ImageDraw.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 81e3d8f46..9df4a5dad 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -318,8 +318,8 @@ Methods Draws a rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param outline: Color to use for the outline. :param fill: Color to use for the fill. :param width: The line width, in pixels. @@ -331,8 +331,8 @@ Methods Draws a rounded rectangle. :param xy: Two points to define the bounding box. Sequence of either - ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``. The bounding box - is inclusive of both endpoints. + ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and + ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. :param outline: Color to use for the outline. :param fill: Color to use for the fill. From 53fb3a9365feec24cb59196477639bf712849ef0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:04:14 +1100 Subject: [PATCH 095/155] Updated lcms2 to 2.15 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 1b5719a8e..55d5ee832 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -150,7 +150,7 @@ Many of Pillow's features require external libraries: * **littlecms** provides color management * Pillow version 2.2.1 and below uses liblcms1, Pillow 2.3.0 and - above uses liblcms2. Tested with **1.19** and **2.7-2.14**. + above uses liblcms2. Tested with **1.19** and **2.7-2.15**. * **libwebp** provides the WebP format. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 35980f19c..3a885afaf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -289,9 +289,9 @@ deps = { # "bins": [r"objs\{msbuild_arch}\Release\freetype.dll"], }, "lcms2": { - "url": SF_PROJECTS + "/lcms/files/lcms/2.14/lcms2-2.14.tar.gz/download", - "filename": "lcms2-2.14.tar.gz", - "dir": "lcms2-2.14", + "url": SF_PROJECTS + "/lcms/files/lcms/2.15/lcms2-2.15.tar.gz/download", + "filename": "lcms2-2.15.tar.gz", + "dir": "lcms2-2.15", "license": "COPYING", "patch": { r"Projects\VC2022\lcms2_static\lcms2_static.vcxproj": { From b84c29a035b2476ae1152fc2054107f25d562dfb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 11:22:35 +1100 Subject: [PATCH 096/155] Raise an error if co-ordinates are incorrectly ordered --- .../imagedraw_ellipse_various_sizes.png | Bin 21446 -> 21600 bytes ...imagedraw_ellipse_various_sizes_filled.png | Bin 20315 -> 20325 bytes Tests/test_imagedraw.py | 28 ++++++++++++++--- src/PIL/ImageDraw.py | 6 ++++ src/_imaging.c | 27 ++++++++++++++++ src/libImaging/Draw.c | 29 ++++++------------ 6 files changed, 66 insertions(+), 24 deletions(-) diff --git a/Tests/images/imagedraw_ellipse_various_sizes.png b/Tests/images/imagedraw_ellipse_various_sizes.png index 11a1be6faebea1a31854e4d13975f1a5c12c811e..5e3cf22b4ad8e19506b7c9551996f522bff75046 100644 GIT binary patch literal 21600 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

L*eghq9S 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 F{vQsP3wQtk literal 20315 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index ea0711ad5..a7267a87e 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -355,7 +355,13 @@ def ellipse_various_sizes_helper(filled): for w in ellipse_sizes: y = 1 for h in ellipse_sizes: - border = [x, y, x + w - 1, y + h - 1] + x1 = x + w + if w: + x1 -= 1 + y1 = y + h + if h: + y1 -= 1 + border = [x, y, x1, y1] if filled: draw.ellipse(border, fill="white") else: @@ -932,9 +938,6 @@ def test_square(): img, draw = create_base_image_draw((10, 10)) draw.rectangle((2, 2, 7, 7), BLACK) assert_image_equal_tofile(img, expected, "square as normal rectangle failed") - img, draw = create_base_image_draw((10, 10)) - draw.rectangle((7, 7, 2, 2), BLACK) - assert_image_equal_tofile(img, expected, "square as inverted rectangle failed") def test_triangle_right(): @@ -1499,3 +1502,20 @@ def test_polygon2(): draw.polygon([(18, 30), (19, 31), (18, 30), (85, 30), (60, 72)], "red") expected = "Tests/images/imagedraw_outline_polygon_RGB.png" assert_image_similar_tofile(im, expected, 1) + + +def test_incorrectly_ordered_coordinates(): + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im) + with pytest.raises(ValueError): + draw.arc((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.chord((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.ellipse((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.pieslice((1, 1, 0, 0), 10, 260) + with pytest.raises(ValueError): + draw.rectangle((1, 1, 0, 0)) + with pytest.raises(ValueError): + draw.rounded_rectangle((1, 1, 0, 0)) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index a55ebbe8e..2d0a98765 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,6 +303,12 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy + if x1 < x0 or y1 < y0: + msg = ( + "x1 must be greater than or equal to x0," + " and y1 must be greater than or equal to y0" + ) + raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index cece2e93a..12d7f93a9 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,6 +251,8 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; +static const char *incorrectly_ordered_coordinates = + "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2805,6 +2807,11 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawArc( self->image->image, @@ -2886,6 +2893,11 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawChord( self->image->image, @@ -2932,6 +2944,11 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawEllipse( self->image->image, @@ -3101,6 +3118,11 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawPieslice( self->image->image, @@ -3197,6 +3219,11 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } + if (xy[2] < xy[0] || xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + free(xy); + return NULL; + } n = ImagingDrawRectangle( self->image->image, diff --git a/src/libImaging/Draw.c b/src/libImaging/Draw.c index 77343e583..82f290bd0 100644 --- a/src/libImaging/Draw.c +++ b/src/libImaging/Draw.c @@ -85,25 +85,22 @@ point32(Imaging im, int x, int y, int ink) { static inline void point32rgba(Imaging im, int x, int y, int ink) { - unsigned int tmp1; + unsigned int tmp; if (x >= 0 && x < im->xsize && y >= 0 && y < im->ysize) { UINT8 *out = (UINT8 *)im->image[y] + x * 4; UINT8 *in = (UINT8 *)&ink; - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); } } static inline void hline8(Imaging im, int x0, int y0, int x1, int ink) { - int tmp, pixelwidth; + int pixelwidth; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -126,13 +123,9 @@ hline8(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; INT32 *p; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -152,13 +145,9 @@ hline32(Imaging im, int x0, int y0, int x1, int ink) { static inline void hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { - int tmp; - unsigned int tmp1; + unsigned int tmp; if (y0 >= 0 && y0 < im->ysize) { - if (x0 > x1) { - tmp = x0, x0 = x1, x1 = tmp; - } if (x0 < 0) { x0 = 0; } else if (x0 >= im->xsize) { @@ -173,9 +162,9 @@ hline32rgba(Imaging im, int x0, int y0, int x1, int ink) { UINT8 *out = (UINT8 *)im->image[y0] + x0 * 4; UINT8 *in = (UINT8 *)&ink; while (x0 <= x1) { - out[0] = BLEND(in[3], out[0], in[0], tmp1); - out[1] = BLEND(in[3], out[1], in[1], tmp1); - out[2] = BLEND(in[3], out[2], in[2], tmp1); + out[0] = BLEND(in[3], out[0], in[0], tmp); + out[1] = BLEND(in[3], out[1], in[1], tmp); + out[2] = BLEND(in[3], out[2], in[2], tmp); x0++; out += 4; } From a4965a7eaa0476ff415bac345d9965101be10ee0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 22:06:40 +1100 Subject: [PATCH 097/155] Split into x and y errors --- Tests/test_imagedraw.py | 15 ++++++------ src/PIL/ImageDraw.py | 10 ++++---- src/_imaging.c | 51 +++++++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index a7267a87e..5295021a3 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1504,18 +1504,19 @@ def test_polygon2(): assert_image_similar_tofile(im, expected, 1) -def test_incorrectly_ordered_coordinates(): +@pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) +def test_incorrectly_ordered_coordinates(xy): im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): - draw.arc((1, 1, 0, 0), 10, 260) + draw.arc(xy, 10, 260) with pytest.raises(ValueError): - draw.chord((1, 1, 0, 0), 10, 260) + draw.chord(xy, 10, 260) with pytest.raises(ValueError): - draw.ellipse((1, 1, 0, 0)) + draw.ellipse(xy) with pytest.raises(ValueError): - draw.pieslice((1, 1, 0, 0), 10, 260) + draw.pieslice(xy, 10, 260) with pytest.raises(ValueError): - draw.rectangle((1, 1, 0, 0)) + draw.rectangle(xy) with pytest.raises(ValueError): - draw.rounded_rectangle((1, 1, 0, 0)) + draw.rounded_rectangle(xy) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 2d0a98765..5a0df09cb 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -303,11 +303,11 @@ class ImageDraw: (x0, y0), (x1, y1) = xy else: x0, y0, x1, y1 = xy - if x1 < x0 or y1 < y0: - msg = ( - "x1 must be greater than or equal to x0," - " and y1 must be greater than or equal to y0" - ) + if x1 < x0: + msg = "x1 must be greater than or equal to x0" + raise ValueError(msg) + if y1 < y0: + msg = "y1 must be greater than or equal to y0" raise ValueError(msg) if corners is None: corners = (True, True, True, True) diff --git a/src/_imaging.c b/src/_imaging.c index 12d7f93a9..1c25ab00c 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -251,8 +251,10 @@ PyImaging_GetBuffer(PyObject *buffer, Py_buffer *view) { static const char *must_be_sequence = "argument must be a sequence"; static const char *must_be_two_coordinates = "coordinate list must contain exactly 2 coordinates"; -static const char *incorrectly_ordered_coordinates = - "x1 must be greater than or equal to x0, and y1 must be greater than or equal to y0"; +static const char *incorrectly_ordered_x_coordinate = + "x1 must be greater than or equal to x0"; +static const char *incorrectly_ordered_y_coordinate = + "y1 must be greater than or equal to y0"; static const char *wrong_mode = "unrecognized image mode"; static const char *wrong_raw_mode = "unrecognized raw mode"; static const char *outside_image = "image index out of range"; @@ -2807,8 +2809,13 @@ _draw_arc(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2893,8 +2900,13 @@ _draw_chord(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -2944,8 +2956,13 @@ _draw_ellipse(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3118,8 +3135,13 @@ _draw_pieslice(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } @@ -3219,8 +3241,13 @@ _draw_rectangle(ImagingDrawObject *self, PyObject *args) { free(xy); return NULL; } - if (xy[2] < xy[0] || xy[3] < xy[1]) { - PyErr_SetString(PyExc_ValueError, incorrectly_ordered_coordinates); + if (xy[2] < xy[0]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_x_coordinate); + free(xy); + return NULL; + } + if (xy[3] < xy[1]) { + PyErr_SetString(PyExc_ValueError, incorrectly_ordered_y_coordinate); free(xy); return NULL; } From 396dd820b937fa42f85a859acb303e29d7b37a76 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 1 Mar 2023 23:04:21 +1100 Subject: [PATCH 098/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d5798d41b..90f97d89f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 + [radarhere] + - Added "corners" argument to ImageDraw rounded_rectangle() #6954 [radarhere] From 1a790a91f57ac764d77fb8e5d23b82669419e391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Mar 2023 14:38:51 +1100 Subject: [PATCH 099/155] Updated harfbuzz to 7.1.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 3a885afaf..2820bdb36 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -356,9 +356,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.0.1.zip", - "filename": "harfbuzz-7.0.1.zip", - "dir": "harfbuzz-7.0.1", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip", + "filename": "harfbuzz-7.1.0.zip", + "dir": "harfbuzz-7.1.0", "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), From 9ed8ca14943f38ef461e5e0d74cb3e19f6ecf4d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Mar 2023 07:50:52 +1100 Subject: [PATCH 100/155] Removed "del im" --- Tests/test_file_dcx.py | 6 ++++-- Tests/test_file_fli.py | 6 ++++-- Tests/test_file_gif.py | 6 ++++-- Tests/test_file_im.py | 6 ++++-- Tests/test_file_mpo.py | 6 ++++-- Tests/test_file_psd.py | 6 ++++-- Tests/test_file_spider.py | 6 ++++-- Tests/test_file_tiff.py | 6 ++++-- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 1adda7729..22686af34 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -24,10 +24,12 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(TEST_FILE) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index cb767a0d8..f96afdc95 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -32,10 +32,12 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(static_test_file) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8f11f0a1c..8522f486a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -32,10 +32,12 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(TEST_GIF) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index bdc704ee1..fd00f260e 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -28,10 +28,12 @@ def test_name_limit(tmp_path): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(TEST_IM) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index ea701f70d..2e921e467 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -38,10 +38,12 @@ def test_sanity(test_file): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(test_files[0]) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index ff78993fe..e405834b5 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -23,10 +23,12 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(test_file) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 122690e34..09f1ef8e4 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -21,10 +21,12 @@ def test_sanity(): @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(): - with pytest.warns(ResourceWarning): + def open(): im = Image.open(TEST_FILE) im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(): diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index d0d9ed891..b40f690f5 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -57,10 +57,12 @@ class TestFileTiff: @pytest.mark.skipif(is_pypy(), reason="Requires CPython") def test_unclosed_file(self): - with pytest.warns(ResourceWarning): + def open(): im = Image.open("Tests/images/multipage.tiff") im.load() - del im + + with pytest.warns(ResourceWarning): + open() def test_closed_file(self): with warnings.catch_warnings(): From 32bfee030bfc30626eb2165c0a69ff181c3bdaef Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 3 Mar 2023 10:29:51 +0200 Subject: [PATCH 101/155] Update Tests/test_file_apng.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_apng.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index 9f850d0e9..b2bec5984 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -266,6 +266,7 @@ def test_apng_chunk_errors(): with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() + assert not im.is_animated with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: assert not im.is_animated From 2299490082f3d53c6cf8a70c88bcef7ebf5329ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Carron?= Date: Fri, 3 Mar 2023 11:41:37 +0100 Subject: [PATCH 102/155] Close the file pointer copy (_fp) in the libtiff encoder if it is still open. --- src/PIL/TiffImagePlugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 04d246dd4..42038831c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1850,6 +1850,11 @@ def _save(im, fp, filename): fp.write(data) if errcode: break + if _fp: + try: + os.close(_fp) + except OSError: + pass if errcode < 0: msg = f"encoder error {errcode} when writing image file" raise OSError(msg) From 912ab3e0889a0475e6edef4149de980345b1eb88 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 3 Mar 2023 16:00:56 +0200 Subject: [PATCH 103/155] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_core_resources.py | 4 ++-- Tests/test_file_webp.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index cb6cde8eb..9021a9fb3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -179,11 +179,11 @@ class TestEnvVars: @pytest.mark.parametrize( "var", - [ + ( {"PILLOW_ALIGNMENT": "15"}, {"PILLOW_BLOCK_SIZE": "1024"}, {"PILLOW_BLOCKS_MAX": "wat"}, - ], + ), ) def test_warnings(self, var): with pytest.warns(UserWarning): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 335201fe1..a7b6c735a 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -31,7 +31,8 @@ class TestUnsupportedWebp: file_path = "Tests/images/hopper.webp" with pytest.warns(UserWarning): with pytest.raises(OSError): - Image.open(file_path) + with Image.open(file_path): + pass if HAVE_WEBP: WebPImagePlugin.SUPPORTED = True From e953978b31954549caf4e6fab185b344644ab0ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Mar 2023 11:44:45 +1100 Subject: [PATCH 104/155] Added test --- Tests/test_file_libtiff.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index f886d3aae..871ad28b5 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1065,3 +1065,9 @@ class TestFileLibTiff(LibTiffTestCase): out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): im.save(out, compression=compression) + + def test_save_many_compressed(self, tmp_path): + im = hopper() + out = str(tmp_path / "temp.tif") + for _ in range(10000): + im.save(out, compression="jpeg") From 02afe1f13bb09df857c01cf820c511846a03df92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Mar 2023 16:35:51 +1100 Subject: [PATCH 105/155] Removed unused profile_fromstring method --- src/_imagingcms.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 9b5a121d7..efb045667 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -116,7 +116,7 @@ cms_profile_open(PyObject *self, PyObject *args) { } static PyObject * -cms_profile_fromstring(PyObject *self, PyObject *args) { +cms_profile_frombytes(PyObject *self, PyObject *args) { cmsHPROFILE hProfile; char *pProfile; @@ -960,8 +960,7 @@ _is_intent_supported(CmsProfileObject *self, int clut) { static PyMethodDef pyCMSdll_methods[] = { {"profile_open", cms_profile_open, METH_VARARGS}, - {"profile_frombytes", cms_profile_fromstring, METH_VARARGS}, - {"profile_fromstring", cms_profile_fromstring, METH_VARARGS}, + {"profile_frombytes", cms_profile_frombytes, METH_VARARGS}, {"profile_tobytes", cms_profile_tobytes, METH_VARARGS}, /* profile and transform functions */ From b970eb9e5d4d11b91cd8fa6b10bb53f18b5c65c4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Mar 2023 17:18:59 +1100 Subject: [PATCH 106/155] Added memoryview support to Dib.frombytes() --- Tests/test_imagewin.py | 11 +++++++---- src/display.c | 10 +++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index 9d64d17a3..5e489284f 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -100,8 +100,11 @@ class TestImageWinDib: # Act # Make one the same as the using tobytes()/frombytes() test_buffer = dib1.tobytes() - dib2.frombytes(test_buffer) + for datatype in ("bytes", "memoryview"): + if datatype == "memoryview": + test_buffer = memoryview(test_buffer) + dib2.frombytes(test_buffer) - # Assert - # Confirm they're the same - assert dib1.tobytes() == dib2.tobytes() + # Assert + # Confirm they're the same + assert dib1.tobytes() == dib2.tobytes() diff --git a/src/display.c b/src/display.c index a50fc3e24..227e306a1 100644 --- a/src/display.c +++ b/src/display.c @@ -195,20 +195,20 @@ _releasedc(ImagingDisplayObject *display, PyObject *args) { static PyObject * _frombytes(ImagingDisplayObject *display, PyObject *args) { - char *ptr; - Py_ssize_t bytes; + Py_buffer buffer; - if (!PyArg_ParseTuple(args, "y#:frombytes", &ptr, &bytes)) { + if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) { return NULL; } - if (display->dib->ysize * display->dib->linesize != bytes) { + if (display->dib->ysize * display->dib->linesize != buffer.len) { PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } - memcpy(display->dib->bits, ptr, bytes); + memcpy(display->dib->bits, buffer.buf, buffer.len); + PyBuffer_Release(&buffer); Py_INCREF(Py_None); return Py_None; } From 96e4e6160eadda1b2291aecb38263af4fb077491 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 4 Mar 2023 21:00:18 +1100 Subject: [PATCH 107/155] Added release notes for #6961 and #6954 --- docs/reference/ImageDraw.rst | 2 +- docs/releasenotes/9.5.0.rst | 60 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 docs/releasenotes/9.5.0.rst diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9df4a5dad..9565ab149 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -338,7 +338,7 @@ Methods :param fill: Color to use for the fill. :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, - `(top_left, top_right, bottom_right, bottom_left)`. + ``(top_left, top_right, bottom_right, bottom_left)``. .. versionadded:: 8.2.0 diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst new file mode 100644 index 000000000..c063d638e --- /dev/null +++ b/docs/releasenotes/9.5.0.rst @@ -0,0 +1,60 @@ +9.5.0 +----- + +Backwards Incompatible Changes +============================== + +TODO +^^^^ + +TODO + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +Added ``dpi`` argument when saving PDFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When saving a PDF, resolution could already be specified using the +``resolution`` argument. Now, a tuple of ``(x_resolution, y_resolution)`` can +be provided as ``dpi``. If both are provided, ``dpi`` will override +``resolution``. + +Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of +``corners``. This a tuple of booleans, specifying whether to round each corner, +``(top_left, top_right, bottom_right, bottom_left)``. + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index a2b588696..177fb65dd 100644 --- a/docs/releasenotes/index.rst +++ b/docs/releasenotes/index.rst @@ -14,6 +14,7 @@ expected to be backported to earlier versions. .. toctree:: :maxdepth: 2 + 9.5.0 9.4.0 9.3.0 9.2.0 From b9c772a889fc0018896d53bbae09830454447a35 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 4 Mar 2023 22:08:36 +1100 Subject: [PATCH 108/155] Capitalised variable type Co-authored-by: Hugo van Kemenade --- docs/releasenotes/9.5.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index c063d638e..df2ec53fa 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -40,7 +40,7 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :py:meth:`.ImageDraw.rounded_rectangle` now accepts a keyword argument of -``corners``. This a tuple of booleans, specifying whether to round each corner, +``corners``. This a tuple of Booleans, specifying whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. Security From cb65bb672b66090b036c123565a783659c3edb44 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Mar 2023 15:44:56 +0200 Subject: [PATCH 109/155] Don't build docs in main tests or trigger main tests for docs-only --- .github/workflows/test-cygwin.yml | 11 ++++++++++- .github/workflows/test-docker.yml | 11 ++++++++++- .github/workflows/test-mingw.yml | 11 ++++++++++- .github/workflows/test-windows.yml | 11 ++++++++++- .github/workflows/test.yml | 16 ++++++++++------ 5 files changed, 50 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 451181434..6c9ed66e3 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -1,6 +1,15 @@ name: Test Cygwin -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 7d2b20d65..f7153386e 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -1,6 +1,15 @@ name: Test Docker -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 737da7b94..ddfafc9d7 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -1,6 +1,15 @@ name: Test MinGW -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 306e34ca9..833f096c3 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -1,6 +1,15 @@ name: Test Windows -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33958bea8..10c3cd929 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,15 @@ name: Test -on: [push, pull_request, workflow_dispatch] +on: + push: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths-ignore: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: permissions: contents: read @@ -96,11 +105,6 @@ jobs: name: errors path: Tests/errors - - name: Docs - if: startsWith(matrix.os, 'ubuntu') && matrix.python-version == 3.11 - run: | - make doccheck - - name: After success run: | .ci/after_success.sh From 06cb3426efec840fd1e5bae1fd136bb77867cbc7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 4 Mar 2023 16:02:36 +0200 Subject: [PATCH 110/155] Build docs in own workflow --- .github/workflows/docs.yml | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..2ae14d468 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,50 @@ +name: Docs + +on: + push: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + + runs-on: ubuntu-latest + name: Docs + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + cache: pip + cache-dependency-path: ".ci/*.sh" + + - name: Build system information + run: python3 .github/workflows/system-info.py + + - name: Install Linux dependencies + run: | + .ci/install.sh + + - name: Build + run: | + .ci/build.sh + + - name: Docs + run: | + make doccheck From 1497e9ef652fc1d9dfe529dc83a83d841d1c47de Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Mar 2023 07:05:49 +1100 Subject: [PATCH 111/155] Run valgrind tests when GitHub Actions file changes --- .github/workflows/test-valgrind.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index f8b050f76..6fab0ecd2 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -5,10 +5,12 @@ name: Test Valgrind on: push: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/test-valgrind.yml" - "**.c" - "**.h" workflow_dispatch: @@ -16,7 +18,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From a7f836187dd3f3f7c220700966f1c4c58a07bcb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 5 Mar 2023 21:51:46 +1100 Subject: [PATCH 112/155] Removed missing anchor from link --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 55d5ee832..f0aa0f399 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -186,8 +186,8 @@ Many of Pillow's features require external libraries: * Pillow wheels since version 8.2.0 include a modified version of libraqm that loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-Link Library Search Order (Microsoft Docs) - `_ + into a directory listed in the `Dynamic-link library search order (Microsoft Docs) + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. From 52cce5293d28394a375b8eb669acdb1cb31f58c0 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 5 Mar 2023 19:14:41 +0000 Subject: [PATCH 113/155] restore link anchor --- docs/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index f0aa0f399..98957335b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -186,8 +186,8 @@ Many of Pillow's features require external libraries: * Pillow wheels since version 8.2.0 include a modified version of libraqm that loads libfribidi at runtime if it is installed. On Windows this requires compiling FriBiDi and installing ``fribidi.dll`` - into a directory listed in the `Dynamic-link library search order (Microsoft Docs) - `_ + into a directory listed in the `Dynamic-link library search order (Microsoft Learn) + `_ (``fribidi-0.dll`` or ``libfribidi-0.dll`` are also detected). See `Build Options`_ to see how to build this version. * Previous versions of Pillow (5.0.0 to 8.1.2) linked libraqm dynamically at runtime. From 494a3bcf2ba00ac5deaa4598b1445ce9f1dfb3ba Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 6 Mar 2023 10:00:51 +1100 Subject: [PATCH 114/155] Release buffer on error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/display.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/display.c b/src/display.c index 227e306a1..e8e7b62c2 100644 --- a/src/display.c +++ b/src/display.c @@ -202,6 +202,7 @@ _frombytes(ImagingDisplayObject *display, PyObject *args) { } if (display->dib->ysize * display->dib->linesize != buffer.len) { + PyBuffer_Release(&buffer); PyErr_SetString(PyExc_ValueError, "wrong size"); return NULL; } From 29b6db4f8a271b0c90d2bf129b40f2f3ca65185d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 6 Mar 2023 10:26:30 +0200 Subject: [PATCH 115/155] Add GHA_PYTHON_VERSION Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/docs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2ae14d468..8a3265476 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,6 +40,8 @@ jobs: - name: Install Linux dependencies run: | .ci/install.sh + env: + GHA_PYTHON_VERSION: "3.x" - name: Build run: | From d93d0a3772cd8255b1b32dd9d71eefe5bac634c1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 6 Mar 2023 10:02:55 +1100 Subject: [PATCH 116/155] Run CIFuzz tests when GitHub Actions file changes --- .github/workflows/cifuzz.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml index db0307046..560d6c7df 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/cifuzz.yml @@ -3,10 +3,12 @@ name: CIFuzz on: push: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" pull_request: paths: + - ".github/workflows/cifuzz.yml" - "**.c" - "**.h" workflow_dispatch: @@ -14,7 +16,7 @@ on: permissions: contents: read -concurrency: +concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true From 2d01875e7c9be1b2a550f143c542bd931fa7c821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:34:44 +1100 Subject: [PATCH 117/155] Added QOI reading --- Tests/images/hopper.qoi | Bin 0 -> 35651 bytes Tests/images/pil123rgba.qoi | Bin 0 -> 42827 bytes Tests/test_file_qoi.py | 28 +++++++ docs/handbook/image-file-formats.rst | 7 ++ docs/releasenotes/9.5.0.rst | 5 ++ src/PIL/QoiImagePlugin.py | 106 +++++++++++++++++++++++++++ src/PIL/__init__.py | 1 + 7 files changed, 147 insertions(+) create mode 100644 Tests/images/hopper.qoi create mode 100644 Tests/images/pil123rgba.qoi create mode 100644 Tests/test_file_qoi.py create mode 100644 src/PIL/QoiImagePlugin.py diff --git a/Tests/images/hopper.qoi b/Tests/images/hopper.qoi new file mode 100644 index 0000000000000000000000000000000000000000..6b255aba13babbe06be9be4b78dc62f47e5b4284 GIT binary patch literal 35651 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 W(eS-{pT)cf{D1iO8@w0)+5QiX6ZyIT literal 0 HcmV?d00001 diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py new file mode 100644 index 000000000..d5c2f3926 --- /dev/null +++ b/Tests/test_file_qoi.py @@ -0,0 +1,28 @@ +import pytest + +from PIL import Image, QoiImagePlugin + +from .helper import assert_image_equal_tofile, assert_image_similar_tofile + + +class TestFileQOI: + def test_sanity(self): + with Image.open("Tests/images/hopper.qoi") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "QOI" + + assert_image_equal_tofile(im, "Tests/images/hopper.png") + + with Image.open("Tests/images/pil123rgba.qoi") as im: + assert im.mode == "RGBA" + assert im.size == (162, 150) + assert im.format == "QOI" + + assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) + + def test_invalid_file(self): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + QoiImagePlugin.QoiImageFile(invalid_file) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02224af34..27a624545 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1544,6 +1544,13 @@ The :py:meth:`~PIL.Image.Image.save` method can take the following keyword argum .. versionadded:: 5.3.0 +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads QOI images. + XV Thumbnails ^^^^^^^^^^^^^ diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index df2ec53fa..ebefc3df7 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -28,6 +28,11 @@ TODO API Additions ============= +QOI file format +^^^^^^^^^^^^^^^ + +Pillow can now read QOI images. + Added ``dpi`` argument when saving PDFs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py new file mode 100644 index 000000000..09aa1a1a4 --- /dev/null +++ b/src/PIL/QoiImagePlugin.py @@ -0,0 +1,106 @@ +# +# The Python Imaging Library. +# $Id$ +# +# QOI support for PIL +# +# See the README file for information on usage and redistribution. +# + +import os + +from . import Image, ImageFile +from ._binary import i32be as i32 +from ._binary import o8 + + +def _accept(prefix): + return prefix[:4] == b"qoif" + + +class QoiImageFile(ImageFile.ImageFile): + format = "QOI" + format_description = "Quite OK Image" + + def _open(self): + if not _accept(self.fp.read(4)): + msg = "not a QOI file" + raise SyntaxError(msg) + + self._size = tuple(i32(self.fp.read(4)) for i in range(2)) + + channels = self.fp.read(1)[0] + self.mode = "RGB" if channels == 3 else "RGBA" + + self.fp.seek(1, os.SEEK_CUR) # colorspace + self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)] + + +class QoiDecoder(ImageFile.PyDecoder): + _pulls_fd = True + + def _add_to_previous_pixels(self, value): + self._previous_pixel = value + + r, g, b, a = value + hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + self._previously_seen_pixels[hash_value] = value + + def decode(self, buffer): + self._previously_seen_pixels = {} + self._previous_pixel = None + self._add_to_previous_pixels(b"".join(o8(i) for i in (0, 0, 0, 255))) + + data = bytearray() + bands = Image.getmodebands(self.mode) + while len(data) < self.state.xsize * self.state.ysize * bands: + byte = self.fd.read(1)[0] + if byte == 0b11111110: # QOI_OP_RGB + value = self.fd.read(3) + o8(255) + elif byte == 0b11111111: # QOI_OP_RGBA + value = self.fd.read(4) + else: + op = byte >> 6 + if op == 0: # QOI_OP_INDEX + op_index = byte & 0b00111111 + value = self._previously_seen_pixels.get(op_index, (0, 0, 0, 0)) + elif op == 1: # QOI_OP_DIFF + value = ( + (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) + % 256, + (self._previous_pixel[1] + ((byte & 0b00001100) >> 2) - 2) + % 256, + (self._previous_pixel[2] + (byte & 0b00000011) - 2) % 256, + ) + value += (self._previous_pixel[3],) + elif op == 2: # QOI_OP_LUMA + second_byte = self.fd.read(1)[0] + diff_green = (byte & 0b00111111) - 32 + diff_red = ((second_byte & 0b11110000) >> 4) - 8 + diff_blue = (second_byte & 0b00001111) - 8 + + value = tuple( + (self._previous_pixel[i] + diff_green + diff) % 256 + for i, diff in enumerate((diff_red, 0, diff_blue)) + ) + value += (self._previous_pixel[3],) + elif op == 3: # QOI_OP_RUN + run_length = (byte & 0b00111111) + 1 + value = self._previous_pixel + if bands == 3: + value = value[:3] + data += value * run_length + continue + value = b"".join(o8(i) for i in value) + self._add_to_previous_pixels(value) + + if bands == 3: + value = value[:3] + data += value + self.set_as_raw(bytes(data)) + return -1, 0 + + +Image.register_open(QoiImageFile.format, QoiImageFile, _accept) +Image.register_decoder("qoi", QoiDecoder) +Image.register_extension(QoiImageFile.format, ".qoi") diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 0e6f82092..32d2381f3 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -59,6 +59,7 @@ _plugins = [ "PngImagePlugin", "PpmImagePlugin", "PsdImagePlugin", + "QoiImagePlugin", "SgiImagePlugin", "SpiderImagePlugin", "SunImagePlugin", From cbde5b262653f1b5273735eab1b63e50fdfadda7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:36:22 +1100 Subject: [PATCH 118/155] Added reading of JPEG2000 comments --- Tests/images/comment.jp2 | Bin 0 -> 209 bytes Tests/test_file_jpeg2k.py | 11 +++++++++++ docs/releasenotes/9.5.0.rst | 6 ++++++ src/PIL/Jpeg2KImagePlugin.py | 24 ++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 Tests/images/comment.jp2 diff --git a/Tests/images/comment.jp2 b/Tests/images/comment.jp2 new file mode 100644 index 0000000000000000000000000000000000000000..4bdf91760e12035fd2c9146ba2a2654c04ffe50b GIT binary patch literal 209 zcmZQzVBpCLP*C9IYUg5LU=T?wsVvAUFj4@r8KAT-kj?;d#WFKeihwjD7&Ef718D{Z z{^b0eB9IURgCG#M02L?y_x~Trpa~X(Cw{^ JIN1N+1ONn(CH(*Z literal 0 HcmV?d00001 diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index de622c478..81795d54c 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -353,6 +353,17 @@ def test_subsampling_decode(name): assert_image_similar(im, expected, epsilon) +def test_comment(): + with Image.open("Tests/images/comment.jp2") as im: + assert im.info["comment"] == b"Created by OpenJPEG version 2.5.0" + + # Test an image that is truncated partway through a codestream + with open("Tests/images/comment.jp2", "rb") as fp: + b = BytesIO(fp.read(130)) + with Image.open(b) as im: + pass + + @pytest.mark.parametrize( "test_file", [ diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index df2ec53fa..e9d35aee2 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -43,6 +43,12 @@ Added ``corners`` argument to ``ImageDraw.rounded_rectangle()`` ``corners``. This a tuple of Booleans, specifying whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. +Reading JPEG comments +^^^^^^^^^^^^^^^^^^^^^ + +When opening a JPEG2000 image, the comment may now be read into +:py:attr:`~PIL.Image.Image.info`. + Security ======== diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1..1dec2f84a 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -218,6 +218,8 @@ class Jpeg2KImageFile(ImageFile.ImageFile): self._size, self.mode, self.custom_mimetype, dpi = header if dpi is not None: self.info["dpi"] = dpi + if self.fp.read(12).endswith(b"jp2c\xff\x4f\xff\x51"): + self._parse_comment() else: msg = "not a JPEG 2000 file" raise SyntaxError(msg) @@ -254,6 +256,28 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ) ] + def _parse_comment(self): + hdr = self.fp.read(2) + length = struct.unpack(">H", hdr)[0] + self.fp.seek(length - 2, os.SEEK_CUR) + + while True: + marker = self.fp.read(2) + if not marker: + break + typ = marker[1] + if typ in (0x90, 0xD9): + # Start of tile or end of codestream + break + hdr = self.fp.read(2) + length = struct.unpack(">H", hdr)[0] + if typ == 0x64: + # Comment + self.info["comment"] = self.fp.read(length - 2)[2:] + break + else: + self.fp.seek(length - 2, os.SEEK_CUR) + @property def reduce(self): # https://github.com/python-pillow/Pillow/issues/4343 found that the From 69325629742a08989424af5e729aaafea8eb6118 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 22:21:37 +1100 Subject: [PATCH 119/155] Added __int__ to IFDRational for Python >= 3.11 --- src/PIL/TiffImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 42038831c..8c0431492 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -425,6 +425,9 @@ class IFDRational(Rational): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): From a334bb6524d38916e39fad44e920ce91cc43eea4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 20:24:34 +1100 Subject: [PATCH 120/155] Updated libimagequant to 4.1.1 --- depends/install_imagequant.sh | 2 +- docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/depends/install_imagequant.sh b/depends/install_imagequant.sh index 8b847b894..362ad95a2 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.0 +archive=libimagequant-4.1.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/docs/installation.rst b/docs/installation.rst index 98957335b..1d38919b1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -169,7 +169,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1** + * Pillow has been tested with libimagequant **2.6-4.1.1** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 347dea12a92a64e4ef768614c8d6b458ff07c42a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 23:12:26 +1100 Subject: [PATCH 121/155] Moved potential error earlier --- src/_webp.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..e8d01f7b2 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -955,6 +955,13 @@ addTransparencyFlagToModule(PyObject *m) { static int setup_module(PyObject *m) { +#ifdef HAVE_WEBPANIM + /* Ready object types */ + if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || + PyType_Ready(&WebPAnimEncoder_Type) < 0) { + return -1; + } +#endif PyObject *d = PyModule_GetDict(m); addMuxFlagToModule(m); addAnimFlagToModule(m); @@ -963,13 +970,6 @@ setup_module(PyObject *m) { PyDict_SetItemString( d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); -#ifdef HAVE_WEBPANIM - /* Ready object types */ - if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || - PyType_Ready(&WebPAnimEncoder_Type) < 0) { - return -1; - } -#endif return 0; } From c63b0ca2106d452311b201f52d4ecb107ffe0c31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 18:32:43 +1100 Subject: [PATCH 122/155] Decrement reference count --- src/_imaging.c | 66 ++++++++++++++++++++++++++++++--------------- src/_imagingcms.c | 3 +++ src/_imagingft.c | 20 ++++++++++---- src/_imagingmorph.c | 6 ++++- src/_webp.c | 11 +++++--- 5 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c..847eed5ce 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3820,15 +3820,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyDict_SetItemString(d, "new_count", PyLong_FromLong(arena->stats_new_count)); - PyDict_SetItemString( - d, "allocated_blocks", PyLong_FromLong(arena->stats_allocated_blocks)); - PyDict_SetItemString( - d, "reused_blocks", PyLong_FromLong(arena->stats_reused_blocks)); - PyDict_SetItemString( - d, "reallocated_blocks", PyLong_FromLong(arena->stats_reallocated_blocks)); - PyDict_SetItemString(d, "freed_blocks", PyLong_FromLong(arena->stats_freed_blocks)); - PyDict_SetItemString(d, "blocks_cached", PyLong_FromLong(arena->blocks_cached)); + PyObject *new_count = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", new_count); + Py_XDECREF(new_count); + + PyObject *allocated_blocks = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", allocated_blocks); + Py_XDECREF(allocated_blocks); + + PyObject *reused_blocks = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", reused_blocks); + Py_XDECREF(reused_blocks); + + PyObject *reallocated_blocks = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", reallocated_blocks); + Py_XDECREF(reallocated_blocks); + + PyObject *freed_blocks = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", freed_blocks); + Py_XDECREF(freed_blocks); + + PyObject *blocks_cached = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", blocks_cached); + Py_XDECREF(blocks_cached); return d; } @@ -4197,16 +4211,18 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyDict_SetItemString( - d, "jpeglib_version", PyUnicode_FromString(ImagingJpegVersion())); + PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); + Py_DECREF(jpeglib_version); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyDict_SetItemString( - d, "jp2klib_version", PyUnicode_FromString(ImagingJpeg2KVersion())); + PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); + Py_DECREF(jp2klib_version); } #endif @@ -4215,8 +4231,9 @@ setup_module(PyObject *m) { have_libjpegturbo = Py_True; #define tostr1(a) #a #define tostr(a) tostr1(a) - PyDict_SetItemString( - d, "libjpeg_turbo_version", PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION))); + PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); + Py_DECREF(libjpeg_turbo_version); #undef tostr #undef tostr1 #else @@ -4230,8 +4247,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyDict_SetItemString( - d, "imagequant_version", PyUnicode_FromString(ImagingImageQuantVersion())); + PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", imagequant_version); + Py_DECREF(imagequant_version); } #else have_libimagequant = Py_False; @@ -4248,16 +4266,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyDict_SetItemString( - d, "zlib_version", PyUnicode_FromString(ImagingZipVersion())); + PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", zlibversion); + Py_DECREF(zlibversion); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyDict_SetItemString( - d, "libtiff_version", PyUnicode_FromString(ImagingTiffVersion())); + PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", libtiff_version); + Py_DECREF(libtiff_version); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4280,7 +4300,9 @@ setup_module(PyObject *m) { Py_INCREF(have_xcb); PyModule_AddObject(m, "HAVE_XCB", have_xcb); - PyDict_SetItemString(d, "PILLOW_VERSION", PyUnicode_FromString(version)); + PyObject *pillow_version = PyUnicode_FromString(version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); + Py_DECREF(pillow_version); return 0; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index efb045667..779f31b9c 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -950,6 +950,8 @@ _is_intent_supported(CmsProfileObject *self, int clut) { return Py_None; } PyDict_SetItem(result, id, entry); + Py_DECREF(id); + Py_DECREF(entry); } return result; } @@ -1532,6 +1534,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } PyDict_SetItemString(d, "littlecms_version", v); + Py_DECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6..8697a74ff 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1129,11 +1129,17 @@ font_getvaraxes(FontObject *self) { axis = master->axis[i]; list_axis = PyDict_New(); - PyDict_SetItemString( - list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); - PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); - PyDict_SetItemString( - list_axis, "maximum", PyLong_FromLong(axis.maximum / 65536)); + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + PyDict_SetItemString(list_axis, "minimum", minimum); + Py_XDECREF(minimum); + + PyObject *def = PyLong_FromLong(axis.def / 65536); + PyDict_SetItemString(list_axis, "default", def); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum); + Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); @@ -1144,6 +1150,7 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); PyDict_SetItemString(list_axis, "name", axis_name); + Py_XDECREF(axis_name); break; } } @@ -1359,6 +1366,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); PyDict_SetItemString(d, "freetype2_version", v); + Py_DECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1376,6 +1384,7 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_RAQM", v); PyDict_SetItemString(d, "HAVE_FRIBIDI", v); PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); + Py_DECREF(v); if (have_raqm) { #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); @@ -1383,6 +1392,7 @@ setup_module(PyObject *m) { v = Py_None; #endif PyDict_SetItemString(d, "raqm_version", v); + Py_DECREF(v); #ifdef FRIBIDI_MAJOR_VERSION { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b616..46a40e96d 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -194,6 +194,7 @@ match(PyObject *self, PyObject *args) { if (lut[lut_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -230,6 +231,7 @@ get_on_pixels(PyObject *self, PyObject *args) { if (row[col_idx]) { PyObject *coordObj = Py_BuildValue("(nn)", col_idx, row_idx); PyList_Append(ret, coordObj); + Py_XDECREF(coordObj); } } } @@ -240,7 +242,9 @@ static int setup_module(PyObject *m) { PyObject *d = PyModule_GetDict(m); - PyDict_SetItemString(d, "__version", PyUnicode_FromString("0.1")); + PyObject *version = PyUnicode_FromString("0.1"); + PyDict_SetItemString(d, "__version", version); + Py_DECREF(version); return 0; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..5575e04f9 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -949,8 +949,10 @@ addAnimFlagToModule(PyObject *m) { void addTransparencyFlagToModule(PyObject *m) { - PyModule_AddObject( - m, "HAVE_TRANSPARENCY", PyBool_FromLong(!WebPDecoderBuggyAlpha())); + PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); + if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { + Py_DECREF(have_transparency); + } } static int @@ -960,8 +962,9 @@ setup_module(PyObject *m) { addAnimFlagToModule(m); addTransparencyFlagToModule(m); - PyDict_SetItemString( - d, "webpdecoder_version", PyUnicode_FromString(WebPDecoderVersion_str())); + PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); + Py_DECREF(webpdecoder_version); #ifdef HAVE_WEBPANIM /* Ready object types */ From 096a8ea99e485c1ac264be9ade814b0e1400fd24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 22:39:11 +1100 Subject: [PATCH 123/155] Fix unclosed file warnings --- Tests/test_file_bufrstub.py | 1 + Tests/test_file_fits.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + Tests/test_imageshow.py | 4 ++-- 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 76f185b9a..a7714c92c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -56,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 3048827e0..6f988729f 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -60,6 +60,7 @@ def test_stub_deprecated(): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) handler = Handler() diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 768ac12bd..dd1c5e7d2 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -56,6 +56,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 98dc5443c..7ca10fac5 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -57,6 +57,7 @@ def test_handler(tmp_path): def load(self, im): self.loaded = True + im.fp.close() return Image.new("RGB", (1, 1)) def save(self, im, fp, filename): diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 3e147a9ef..eda485cf6 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -55,8 +55,8 @@ def test_show_without_viewers(): viewers = ImageShow._viewers ImageShow._viewers = [] - im = hopper() - assert not ImageShow.show(im) + with hopper() as im: + assert not ImageShow.show(im) ImageShow._viewers = viewers From f9cbc2e084e63d6ac7b0418efdd5aed52f04c4a8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 11 Mar 2023 23:11:43 +1100 Subject: [PATCH 124/155] Close OleFileIO instance when closing or exiting FPX or MIC --- Tests/test_file_fpx.py | 10 ++++++++++ Tests/test_file_mic.py | 10 ++++++++++ src/PIL/FpxImagePlugin.py | 8 ++++++++ src/PIL/MicImagePlugin.py | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index fa22e90f6..9a1784d31 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -18,6 +18,16 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") +def test_close(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 464d138e2..2588d3a05 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -51,6 +51,16 @@ def test_seek(): assert im.tell() == 0 +def test_close(): + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index d145d01f7..2450c67e9 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -235,6 +235,14 @@ class FpxImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 8dd9f2909..58f7327bd 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -89,6 +89,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- From 2fc7cfb6b256ef21ee7991604e972fd9e5e95efe Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Mar 2023 22:32:38 +1100 Subject: [PATCH 125/155] Added spaces between parameter arguments Co-authored-by: Hugo van Kemenade --- Tests/test_file_libtiff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 21af32db6..7a94c0302 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -985,7 +985,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/old-style-jpeg-compression.png") @pytest.mark.parametrize( - "file_name,mode,size,tile", + "file_name, mode, size, tile", [ ( "tiff_wrong_bits_per_sample.tiff", From 28b8b6088e1a9ab87b96d5d7edd7fcbc08a43ea7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Mar 2023 22:58:49 +1100 Subject: [PATCH 126/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 90f97d89f..cbf91baff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Close OleFileIO instance when closing or exiting FPX or MIC #7005 + [radarhere] + +- Added __int__ to IFDRational for Python >= 3.11 #6998 + [radarhere] + +- Added memoryview support to Dib.frombytes() #6988 + [radarhere, nulano] + +- Close file pointer copy in the libtiff encoder if still open #6986 + [fcarron, radarhere] + - Raise an error if ImageDraw co-ordinates are incorrectly ordered #6978 [radarhere] From 7670736e18026994d521e94a18f34feb194bd7e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Mar 2023 23:17:39 +1100 Subject: [PATCH 127/155] Use type hint Co-authored-by: Hugo van Kemenade --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0c8b390de..19bbf8598 100755 --- a/setup.py +++ b/setup.py @@ -242,9 +242,9 @@ def _find_include_dir(self, dirname, include): return subdir -def _cmd_exists(cmd): +def _cmd_exists(cmd: str) -> bool: if "PATH" not in os.environ: - return + return False return any( os.access(os.path.join(path, cmd), os.X_OK) for path in os.environ["PATH"].split(os.pathsep) From 0a6092b0e6e2446e0a6be4dc698d99552cf0d99e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Mar 2023 23:25:38 +1100 Subject: [PATCH 128/155] Use full name of format Co-authored-by: Hugo van Kemenade --- docs/handbook/image-file-formats.rst | 2 +- docs/releasenotes/9.5.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 27a624545..5ab484df2 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1549,7 +1549,7 @@ QOI .. versionadded:: 9.5.0 -Pillow identifies and reads QOI images. +Pillow identifies and reads images in Quite OK Image format. XV Thumbnails ^^^^^^^^^^^^^ diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index ebefc3df7..7bab9fed7 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -31,7 +31,7 @@ API Additions QOI file format ^^^^^^^^^^^^^^^ -Pillow can now read QOI images. +Pillow can now read images in Quite OK Image format. Added ``dpi`` argument when saving PDFs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From cdb9ca9ea1f08b860a917b55f56c8bfa69071d6b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Mar 2023 23:28:29 +1100 Subject: [PATCH 129/155] Removed class --- Tests/test_file_qoi.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Tests/test_file_qoi.py b/Tests/test_file_qoi.py index d5c2f3926..f33eada61 100644 --- a/Tests/test_file_qoi.py +++ b/Tests/test_file_qoi.py @@ -5,24 +5,24 @@ from PIL import Image, QoiImagePlugin from .helper import assert_image_equal_tofile, assert_image_similar_tofile -class TestFileQOI: - def test_sanity(self): - with Image.open("Tests/images/hopper.qoi") as im: - assert im.mode == "RGB" - assert im.size == (128, 128) - assert im.format == "QOI" +def test_sanity(): + with Image.open("Tests/images/hopper.qoi") as im: + assert im.mode == "RGB" + assert im.size == (128, 128) + assert im.format == "QOI" - assert_image_equal_tofile(im, "Tests/images/hopper.png") + assert_image_equal_tofile(im, "Tests/images/hopper.png") - with Image.open("Tests/images/pil123rgba.qoi") as im: - assert im.mode == "RGBA" - assert im.size == (162, 150) - assert im.format == "QOI" + with Image.open("Tests/images/pil123rgba.qoi") as im: + assert im.mode == "RGBA" + assert im.size == (162, 150) + assert im.format == "QOI" - assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) + assert_image_similar_tofile(im, "Tests/images/pil123rgba.png", 0.03) - def test_invalid_file(self): - invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): - QoiImagePlugin.QoiImageFile(invalid_file) +def test_invalid_file(): + invalid_file = "Tests/images/flower.jpg" + + with pytest.raises(SyntaxError): + QoiImagePlugin.QoiImageFile(invalid_file) From 4e5e7e09756431f4ba59dc927520fe0750839153 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Mar 2023 23:49:06 +1100 Subject: [PATCH 130/155] Added release notes for #6925 --- docs/releasenotes/9.5.0.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index df2ec53fa..19c7b3be3 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -54,7 +54,7 @@ TODO Other Changes ============= -TODO -^^^^ +Added support for saving PDFs in RGBA mode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Using the JPXDecode filter, PDFs can now be saved in RGBA mode. From 56f9b85ad925a7f60239efa32c159bc2eea45014 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 13 Mar 2023 00:03:08 +1100 Subject: [PATCH 131/155] Removed unnecessary line Co-authored-by: Hugo van Kemenade --- src/PIL/QoiImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 09aa1a1a4..ef91b90ab 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -1,6 +1,5 @@ # # The Python Imaging Library. -# $Id$ # # QOI support for PIL # From da2083fb8dc142c7252cb582212dbb6b202f30dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 00:07:53 +1100 Subject: [PATCH 132/155] List modes that can be used when saving PDFs --- docs/handbook/image-file-formats.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 02224af34..6ac56dc30 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1457,8 +1457,13 @@ PDF ^^^ Pillow can write PDF (Acrobat) images. Such images are written as binary PDF 1.4 -files, using either JPEG or HEX encoding depending on the image mode (and -whether JPEG support is available or not). +files. Different encoding methods are used, depending on the image mode. + +* 1 mode images are saved using TIFF encoding, or JPEG encoding if libtiff support is + unavailable +* L, RGB and CMYK mode images use JPEG encoding +* P mode images use HEX encoding +* RGBA mode images use JPEG2000 encoding .. _pdf-saving: From 079caf671120894c6cd0e1d9dc8429970d023847 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 08:26:31 +1100 Subject: [PATCH 133/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cbf91baff..f0f3044a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added QOI reading #6852 + [radarhere, hugovk] + +- Added saving RGBA images as PDFs #6925 + [radarhere] + +- Do not raise an error if os.environ does not contain PATH #6935 + [radarhere, hugovk] + - Close OleFileIO instance when closing or exiting FPX or MIC #7005 [radarhere] From 023d4349e43b4c0965ddee65e481b7322f715412 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 17:00:07 +1100 Subject: [PATCH 134/155] Added release notes for #6834 --- docs/releasenotes/9.5.0.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bd6e58693..13c99313a 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -63,3 +63,10 @@ Added support for saving PDFs in RGBA mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + + +Improved I;16N support +^^^^^^^^^^^^^^^^^^^^^^ + +Support has been added for I;16N access, packing and unpacking. Conversion to +and from L mode has also been added. From 31669013d420c2a532f5be6c5631827fe36e6f70 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 23:13:34 +1100 Subject: [PATCH 135/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f0f3044a3..d7b5b4dab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Improved I;16N support #6834 + [radarhere] + - Added QOI reading #6852 [radarhere, hugovk] From 1f1ab16631724f51473872900246604d5366b059 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 14 Mar 2023 22:35:26 +0200 Subject: [PATCH 136/155] Remove EOL Debian 10 Buster from CI --- .github/workflows/test-docker.yml | 1 - docs/installation.rst | 2 -- 2 files changed, 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index f7153386e..ff1605ac7 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -37,7 +37,6 @@ jobs: centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-10-buster-x86, debian-11-bullseye-x86, fedora-36-amd64, fedora-37-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 1d38919b1..6164a638b 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -432,8 +432,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | -+----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ | Fedora 36 | 3.10 | x86-64 | From b168ec2606353af97302c1cc69d119cfe0103b7c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:07:14 +0200 Subject: [PATCH 137/155] Replace dependency sphinx-issues with builtin sphinx.ext.extlinks --- docs/Makefile | 2 +- docs/conf.py | 15 ++++++++++++--- docs/deprecations.rst | 2 +- docs/releasenotes/3.1.1.rst | 6 +++--- docs/releasenotes/3.1.2.rst | 2 +- docs/releasenotes/6.2.0.rst | 2 +- docs/releasenotes/6.2.2.rst | 8 ++++---- docs/releasenotes/7.1.0.rst | 10 +++++----- docs/releasenotes/8.0.1.rst | 2 +- docs/releasenotes/8.1.0.rst | 8 ++++---- docs/releasenotes/8.1.1.rst | 10 +++++----- docs/releasenotes/8.1.2.rst | 4 ++-- docs/releasenotes/8.2.0.rst | 20 ++++++++++---------- docs/releasenotes/8.3.0.rst | 2 +- docs/releasenotes/8.3.2.rst | 2 +- docs/releasenotes/9.0.0.rst | 6 +++--- docs/releasenotes/9.0.1.rst | 4 ++-- docs/releasenotes/9.1.1.rst | 2 +- setup.cfg | 1 - 19 files changed, 58 insertions(+), 50 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index d32d25a3c..4a3de67cb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,7 +45,7 @@ clean: -rm -rf $(BUILDDIR)/* install-sphinx: - $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-issues sphinx-removed-in sphinxext-opengraph + $(PYTHON) -m pip install --quiet furo olefile sphinx sphinx-copybutton sphinx-inline-tabs sphinx-removed-in sphinxext-opengraph .PHONY: html html: diff --git a/docs/conf.py b/docs/conf.py index e1ffa49b8..683ff7856 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,10 +29,10 @@ needs_sphinx = "2.4" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", + "sphinx.ext.extlinks", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", - "sphinx_issues", "sphinx_removed_in", "sphinxext.opengraph", ] @@ -317,8 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") -# GitHub repo for sphinx-issues -issues_github_path = "python-pillow/Pillow" +# sphinx.ext.extlinks +# This config is a dictionary of external sites, +# mapping unique short aliases to a base URL and a prefix. +# https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html +_repo = "https://github.com/python-pillow/Pillow/" +extlinks = { + "cve": ("https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-%s", "CVE-%s"), + "cwe": ("https://cwe.mitre.org/data/definitions/%s.html", "CWE-%s"), + "issue": (_repo + "issues/%s", "#%s"), + "pr": (_repo + "pull/%s", "#%s"), +} # sphinxext.opengraph ogp_image = ( diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 0db19a64e..a9c6d1f7e 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -261,7 +261,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ diff --git a/docs/releasenotes/3.1.1.rst b/docs/releasenotes/3.1.1.rst index 38118ea39..5d60e116c 100644 --- a/docs/releasenotes/3.1.1.rst +++ b/docs/releasenotes/3.1.1.rst @@ -6,7 +6,7 @@ CVE-2016-0740 -- Buffer overflow in TiffDecode.c ------------------------------------------------ Pillow 3.1.0 and earlier when linked against libtiff >= 4.0.0 on x64 -may overflow a buffer when reading a specially crafted tiff file (:cve:`CVE-2016-0740`). +may overflow a buffer when reading a specially crafted tiff file (:cve:`2016-0740`). Specifically, libtiff >= 4.0.0 changed the return type of ``TIFFScanlineSize`` from ``int32`` to machine dependent @@ -24,7 +24,7 @@ CVE-2016-0775 -- Buffer overflow in FliDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, FliDecode.c has a buffer overflow error (:cve:`CVE-2016-0775`). +release, FliDecode.c has a buffer overflow error (:cve:`2016-0775`). Around line 192: @@ -53,7 +53,7 @@ CVE-2016-2533 -- Buffer overflow in PcdDecode.c ----------------------------------------------- In all versions of Pillow, dating back at least to the last PIL 1.1.7 -release, ``PcdDecode.c`` has a buffer overflow error (:cve:`CVE-2016-2533`). +release, ``PcdDecode.c`` has a buffer overflow error (:cve:`2016-2533`). The ``state.buffer`` for ``PcdDecode.c`` is allocated based on a 3 bytes per pixel sizing, where ``PcdDecode.c`` wrote into the buffer diff --git a/docs/releasenotes/3.1.2.rst b/docs/releasenotes/3.1.2.rst index b5f7cfe99..04325ad86 100644 --- a/docs/releasenotes/3.1.2.rst +++ b/docs/releasenotes/3.1.2.rst @@ -7,7 +7,7 @@ CVE-2016-3076 -- Buffer overflow in Jpeg2KEncode.c Pillow between 2.5.0 and 3.1.1 may overflow a buffer when writing large Jpeg2000 files, allowing for code execution or other memory -corruption (:cve:`CVE-2016-3076`). +corruption (:cve:`2016-3076`). This occurs specifically in the function ``j2k_encode_entry``, at the line: diff --git a/docs/releasenotes/6.2.0.rst b/docs/releasenotes/6.2.0.rst index 0fb33de75..7daac1b19 100644 --- a/docs/releasenotes/6.2.0.rst +++ b/docs/releasenotes/6.2.0.rst @@ -69,7 +69,7 @@ Security ======== This release catches several buffer overruns, as well as addressing -:cve:`CVE-2019-16865`. The CVE is regarding DOS problems, such as consuming large +:cve:`2019-16865`. The CVE is regarding DOS problems, such as consuming large amounts of memory, or taking a large amount of time to process an image. In RawDecode.c, an error is now thrown if skip is calculated to be less than diff --git a/docs/releasenotes/6.2.2.rst b/docs/releasenotes/6.2.2.rst index 79d4b88aa..47692a3de 100644 --- a/docs/releasenotes/6.2.2.rst +++ b/docs/releasenotes/6.2.2.rst @@ -6,13 +6,13 @@ Security This release addresses several security problems. -:cve:`CVE-2019-19911` is regarding FPX images. If an image reports that it has a large +:cve:`2019-19911` is regarding FPX images. If an image reports that it has a large number of bands, a large amount of resources will be used when trying to process the image. This is fixed by limiting the number of bands to those usable by Pillow. -Buffer overruns were found when processing an SGI (:cve:`CVE-2020-5311`), -PCX (:cve:`CVE-2020-5312`) or FLI image (:cve:`CVE-2020-5313`). Checks have been added +Buffer overruns were found when processing an SGI (:cve:`2020-5311`), +PCX (:cve:`2020-5312`) or FLI image (:cve:`2020-5313`). Checks have been added to prevent this. -:cve:`CVE-2020-5310`: Overflow checks have been added when calculating the size of a +:cve:`2020-5310`: Overflow checks have been added when calculating the size of a memory block to be reallocated in the processing of a TIFF image. diff --git a/docs/releasenotes/7.1.0.rst b/docs/releasenotes/7.1.0.rst index cb46f127c..6e231464e 100644 --- a/docs/releasenotes/7.1.0.rst +++ b/docs/releasenotes/7.1.0.rst @@ -72,11 +72,11 @@ Security This release includes security fixes. -* :cve:`CVE-2020-10177` Fix multiple out-of-bounds reads in FLI decoding -* :cve:`CVE-2020-10378` Fix bounds overflow in PCX decoding -* :cve:`CVE-2020-10379` Fix two buffer overflows in TIFF decoding -* :cve:`CVE-2020-10994` Fix bounds overflow in JPEG 2000 decoding -* :cve:`CVE-2020-11538` Fix buffer overflow in SGI-RLE decoding +* :cve:`2020-10177` Fix multiple out-of-bounds reads in FLI decoding +* :cve:`2020-10378` Fix bounds overflow in PCX decoding +* :cve:`2020-10379` Fix two buffer overflows in TIFF decoding +* :cve:`2020-10994` Fix bounds overflow in JPEG 2000 decoding +* :cve:`2020-11538` Fix buffer overflow in SGI-RLE decoding Other Changes ============= diff --git a/docs/releasenotes/8.0.1.rst b/docs/releasenotes/8.0.1.rst index 3584a5d72..f7a1cea65 100644 --- a/docs/releasenotes/8.0.1.rst +++ b/docs/releasenotes/8.0.1.rst @@ -4,7 +4,7 @@ Security ======== -Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`CVE-2020-15999`: +Update FreeType used in binary wheels to `2.10.4`_ to fix :cve:`2020-15999`: - A heap buffer overflow has been found in the handling of embedded PNG bitmaps, introduced in FreeType version 2.6. diff --git a/docs/releasenotes/8.1.0.rst b/docs/releasenotes/8.1.0.rst index 8ed1d9d85..69726e628 100644 --- a/docs/releasenotes/8.1.0.rst +++ b/docs/releasenotes/8.1.0.rst @@ -11,7 +11,7 @@ Support for FreeType 2.7 is deprecated and will be removed in Pillow 9.0.0 (2022 when FreeType 2.8 will be the minimum supported. We recommend upgrading to at least FreeType `2.10.4`_, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _2.10.4: https://sourceforge.net/projects/freetype/files/freetype2/2.10.4/ @@ -40,13 +40,13 @@ This release includes security fixes. * An out-of-bounds read when saving TIFFs with custom metadata through LibTIFF * An out-of-bounds read when saving a GIF of 1px width -* :cve:`CVE-2020-35653` Buffer read overrun in PCX decoding +* :cve:`2020-35653` Buffer read overrun in PCX decoding The PCX image decoder used the reported image stride to calculate the row buffer, rather than calculating it from the image size. This issue dates back to the PIL fork. Thanks to Google's `OSS-Fuzz`_ project for finding this. -* :cve:`CVE-2020-35654` Fix TIFF out-of-bounds write error +* :cve:`2020-35654` Fix TIFF out-of-bounds write error Out-of-bounds write in ``TiffDecode.c`` when reading corrupt YCbCr files in some LibTIFF versions (4.1.0/Ubuntu 20.04, but not 4.0.9/Ubuntu 18.04). In some cases @@ -55,7 +55,7 @@ an out-of-bounds write in ``TiffDecode.c``. This potentially affects Pillow vers from 6.0.0 to 8.0.1, depending on the version of LibTIFF. This was reported through `Tidelift`_. -* :cve:`CVE-2020-35655` Fix for SGI Decode buffer overrun +* :cve:`2020-35655` Fix for SGI Decode buffer overrun 4 byte read overflow in ``SgiRleDecode.c``, where the code was not correctly checking the offsets and length tables. Independently reported through `Tidelift`_ and Google's diff --git a/docs/releasenotes/8.1.1.rst b/docs/releasenotes/8.1.1.rst index 4081c49ca..18d0a33f1 100644 --- a/docs/releasenotes/8.1.1.rst +++ b/docs/releasenotes/8.1.1.rst @@ -4,19 +4,19 @@ Security ======== -:cve:`CVE-2021-25289`: The previous fix for :cve:`CVE-2020-35654` was insufficient +:cve:`2021-25289`: The previous fix for :cve:`2020-35654` was insufficient due to incorrect error checking in ``TiffDecode.c``. -:cve:`CVE-2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` +:cve:`2021-25290`: In ``TiffDecode.c``, there is a negative-offset ``memcpy`` with an invalid size. -:cve:`CVE-2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to +:cve:`2021-25291`: In ``TiffDecode.c``, invalid tile boundaries could lead to an out-of-bounds read in ``TIFFReadRGBATile``. -:cve:`CVE-2021-25292`: The PDF parser has a catastrophic backtracking regex +:cve:`2021-25292`: The PDF parser has a catastrophic backtracking regex that could be used as a DOS attack. -:cve:`CVE-2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, +:cve:`2021-25293`: There is an out-of-bounds read in ``SgiRleDecode.c``, since Pillow 4.3.0. diff --git a/docs/releasenotes/8.1.2.rst b/docs/releasenotes/8.1.2.rst index 50d132f33..de50a3f1d 100644 --- a/docs/releasenotes/8.1.2.rst +++ b/docs/releasenotes/8.1.2.rst @@ -4,8 +4,8 @@ Security ======== -There is an exhaustion of memory DOS in the BLP (:cve:`CVE-2021-27921`), -ICNS (:cve:`CVE-2021-27922`) and ICO (:cve:`CVE-2021-27923`) container formats +There is an exhaustion of memory DOS in the BLP (:cve:`2021-27921`), +ICNS (:cve:`2021-27922`) and ICO (:cve:`2021-27923`) container formats where Pillow did not properly check the reported size of the contained image. These images could cause arbitrarily large memory allocations. This was reported by Jiayi Lin, Luke Shaffer, Xinran Xie, and Akshay Ajayan of diff --git a/docs/releasenotes/8.2.0.rst b/docs/releasenotes/8.2.0.rst index f11953168..452077f1a 100644 --- a/docs/releasenotes/8.2.0.rst +++ b/docs/releasenotes/8.2.0.rst @@ -129,15 +129,15 @@ Security These were all found with `OSS-Fuzz`_. -:cve:`CVE-2021-25287`, :cve:`CVE-2021-25288`: Fix OOB read in Jpeg2KDecode -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-25287`, :cve:`2021-25288`: Fix OOB read in Jpeg2KDecode +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * For J2k images with multiple bands, it's legal to have different widths for each band, e.g. 1 byte for ``L``, 4 bytes for ``A``. * This dates to Pillow 2.4.0. -:cve:`CVE-2021-28675`: Fix DOS in PsdImagePlugin -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28675`: Fix DOS in PsdImagePlugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * :py:class:`.PsdImagePlugin.PsdImageFile` did not sanity check the number of input layers with regard to the size of the data block, this could lead to a @@ -145,15 +145,15 @@ These were all found with `OSS-Fuzz`_. :py:meth:`~PIL.Image.Image.load`. * This dates to the PIL fork. -:cve:`CVE-2021-28676`: Fix FLI DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28676`: Fix FLI DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``FliDecode.c`` did not properly check that the block advance was non-zero, potentially leading to an infinite loop on load. * This dates to the PIL fork. -:cve:`CVE-2021-28677`: Fix EPS DOS on _open -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28677`: Fix EPS DOS on _open +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * The readline used in EPS has to deal with any combination of ``\r`` and ``\n`` as line endings. It accidentally used a quadratic method of accumulating lines while looking @@ -162,8 +162,8 @@ These were all found with `OSS-Fuzz`_. open phase, before an image was accepted for opening. * This dates to the PIL fork. -:cve:`CVE-2021-28678`: Fix BLP DOS -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:cve:`2021-28678`: Fix BLP DOS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * ``BlpImagePlugin`` did not properly check that reads after jumping to file offsets returned data. This could lead to a denial-of-service where the decoder could be run a diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index 0bfead144..b9642576f 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -85,7 +85,7 @@ Security Buffer overflow ^^^^^^^^^^^^^^^ -This release addresses :cve:`CVE-2021-34552`. PIL since 1.1.4 and Pillow since 1.0 +This release addresses :cve:`2021-34552`. PIL since 1.1.4 and Pillow since 1.0 allowed parameters passed into a convert function to trigger buffer overflow in Convert.c. diff --git a/docs/releasenotes/8.3.2.rst b/docs/releasenotes/8.3.2.rst index 6b5c759fc..3333d63a1 100644 --- a/docs/releasenotes/8.3.2.rst +++ b/docs/releasenotes/8.3.2.rst @@ -4,7 +4,7 @@ Security ======== -* :cve:`CVE-2021-23437`: Avoid a potential ReDoS (regular expression denial of service) +* :cve:`2021-23437`: Avoid a potential ReDoS (regular expression denial of service) in :py:class:`~PIL.ImageColor`'s :py:meth:`~PIL.ImageColor.getrgb` by raising :py:exc:`ValueError` if the color specifier is too long. Present since Pillow 5.2.0. diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index 616cf4aa3..73e77ad3e 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -43,7 +43,7 @@ FreeType 2.7 Support for FreeType 2.7 has been removed; FreeType 2.8 is the minimum supported. We recommend upgrading to at least `FreeType`_ 2.10.4, which fixed a severe -vulnerability introduced in FreeType 2.6 (:cve:`CVE-2020-15999`). +vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). .. _FreeType: https://freetype.org/ @@ -119,7 +119,7 @@ Google's `OSS-Fuzz`_ project for finding this issue. Restrict builtins available to ImageMath.eval ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow +:cve:`2022-22817`: To limit :py:class:`PIL.ImageMath` to working with images, Pillow will now restrict the builtins available to :py:meth:`PIL.ImageMath.eval`. This will help prevent problems arising if users evaluate arbitrary expressions, such as ``ImageMath.eval("exec(exit())")``. @@ -127,7 +127,7 @@ help prevent problems arising if users evaluate arbitrary expressions, such as Fixed ImagePath.Path array handling ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -:cve:`CVE-2022-22815` (:cwe:`CWE-126`) and :cve:`CVE-2022-22816` (:cwe:`CWE-665`) were +:cve:`2022-22815` (:cwe:`126`) and :cve:`2022-22816` (:cwe:`665`) were found when initializing ``ImagePath.Path``. .. _OSS-Fuzz: https://github.com/google/oss-fuzz diff --git a/docs/releasenotes/9.0.1.rst b/docs/releasenotes/9.0.1.rst index c1feee088..acb92dc41 100644 --- a/docs/releasenotes/9.0.1.rst +++ b/docs/releasenotes/9.0.1.rst @@ -6,12 +6,12 @@ Security This release addresses several security problems. -:cve:`CVE-2022-24303`: If the path to the temporary directory on Linux or macOS +:cve:`2022-24303`: If the path to the temporary directory on Linux or macOS contained a space, this would break removal of the temporary image file after ``im.show()`` (and related actions), and potentially remove an unrelated file. This has been present since PIL. -:cve:`CVE-2022-22817`: While Pillow 9.0 restricted top-level builtins available to +:cve:`2022-22817`: While Pillow 9.0 restricted top-level builtins available to :py:meth:`PIL.ImageMath.eval`, it did not prevent builtins available to lambda expressions. These are now also restricted. diff --git a/docs/releasenotes/9.1.1.rst b/docs/releasenotes/9.1.1.rst index f8b155f3d..bab70f8f9 100644 --- a/docs/releasenotes/9.1.1.rst +++ b/docs/releasenotes/9.1.1.rst @@ -6,7 +6,7 @@ Security This release addresses several security problems. -:cve:`CVE-2022-30595`: When reading a TGA file with RLE packets that cross scan lines, +:cve:`2022-30595`: When reading a TGA file with RLE packets that cross scan lines, Pillow reads the information past the end of the first line without deducting that from the length of the remaining file data. This vulnerability was introduced in Pillow 9.1.0, and can cause a heap buffer overflow. diff --git a/setup.cfg b/setup.cfg index 824cae088..d6057f159 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,6 @@ docs = sphinx>=2.4 sphinx-copybutton sphinx-inline-tabs - sphinx-issues>=3.0.1 sphinx-removed-in sphinxext-opengraph tests = From 7703042301af8b5944fb708410b92fc98c042bdc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:20:43 +0200 Subject: [PATCH 138/155] Add missing html target to top-level Makefile (it's in the help) --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a2545b54e..206b1cebe 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,8 @@ coverage: python3 -m coverage report .PHONY: doc -doc: +.PHONY: html +doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html From 542d25cceef7a706c6ee694ed574c3831de27b9d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 8 Mar 2023 18:22:33 +0200 Subject: [PATCH 139/155] Add 'make htmlview' to open index page --- Makefile | 6 ++++++ docs/Makefile | 5 +++++ docs/make.bat | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/Makefile b/Makefile index 206b1cebe..1691e4657 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,11 @@ doc html: python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . $(MAKE) -C docs html +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + .PHONY: doccheck doccheck: $(MAKE) doc @@ -40,6 +45,7 @@ help: @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" diff --git a/docs/Makefile b/docs/Makefile index 4a3de67cb..3b4deb9bf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -19,6 +19,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" + @echo " htmlview to open the index page built by the html target in your browser" @echo " serve to start a local server for viewing docs" @echo " livehtml to start a local server for viewing docs and auto-reload on change" @echo " dirhtml to make HTML files named index.html in directories" @@ -196,6 +197,10 @@ doctest: @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." +.PHONY: htmlview +htmlview: html + $(PYTHON) -c "import os, webbrowser; webbrowser.open('file://' + os.path.realpath('$(BUILDDIR)/html/index.html'))" + .PHONY: livehtml livehtml: html livereload $(BUILDDIR)/html -p 33233 diff --git a/docs/make.bat b/docs/make.bat index c943319ad..ad720cdcd 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -19,6 +19,7 @@ if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files + echo. htmlview to open the index page built by the html target in your browser echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files @@ -52,6 +53,17 @@ if "%1" == "html" ( goto end ) +if "%1" == "htmlview" ( + cmd /C %this% html + + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) + + goto end +) + if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 From f046df2aba896bdc5b2c17a7fd892ef7ea5eff13 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 15 Mar 2023 14:17:42 +0200 Subject: [PATCH 140/155] Add colour to CI for readability --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a3265476..81ba8ef15 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,6 +18,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + FORCE_COLOR: 1 + jobs: build: From 4d77a579ba03f1eb1e8166b5761f515b67a5c9da Mon Sep 17 00:00:00 2001 From: "Cimon Lucas (LCM)" Date: Wed, 15 Mar 2023 19:24:33 +0100 Subject: [PATCH 141/155] [Doc] Minor improvement made to c_extension_debugging.rst --- docs/reference/c_extension_debugging.rst | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index dc4c2bf94..7f29d2f24 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -10,19 +10,13 @@ distributions. - ``python3-dbg`` package for the gdb extensions and python symbols - ``gdb`` and ``valgrind`` -- Potentially debug symbols for libraries. On ubuntu they're shipped - in package-dbgsym packages, from a different repo. +- Potentially debug symbols for libraries. On Ubuntu you can follow those + instructions to install the corresponding packages: `Debug Symbol Packages `_ -:: +Then ``sudo apt-get install libtiff5-dbgsym`` - deb http://ddebs.ubuntu.com focal main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-updates main restricted universe multiverse - deb http://ddebs.ubuntu.com focal-proposed main restricted universe multiverse - -Then ``sudo apt-get update && sudo apt-get install libtiff5-dbgsym`` - -- There's a bug with the dbg package for at least python 3.8 on ubuntu - 20.04, and you need to add a new link or two to make it autoload when +- There's a bug with the ``python3-dbg`` package for at least python 3.8 on + Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: :: From 1bded8335766604d50edf19bd37d068a5cbb3364 Mon Sep 17 00:00:00 2001 From: Lucas Cimon <925560+Lucas-C@users.noreply.github.com> Date: Thu, 16 Mar 2023 07:52:19 +0100 Subject: [PATCH 142/155] Update docs/reference/c_extension_debugging.rst Co-authored-by: Hugo van Kemenade --- docs/reference/c_extension_debugging.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/c_extension_debugging.rst b/docs/reference/c_extension_debugging.rst index 7f29d2f24..5e8586905 100644 --- a/docs/reference/c_extension_debugging.rst +++ b/docs/reference/c_extension_debugging.rst @@ -15,7 +15,7 @@ distributions. Then ``sudo apt-get install libtiff5-dbgsym`` -- There's a bug with the ``python3-dbg`` package for at least python 3.8 on +- There's a bug with the ``python3-dbg`` package for at least Python 3.8 on Ubuntu 20.04, and you need to add a new link or two to make it autoload when running python: From 5080d3581694d1a73a38845d17630808dd3fea10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Mar 2023 11:27:58 +1100 Subject: [PATCH 143/155] Allow libtiff_support_custom_tags to be missing --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 8c0431492..3d4d0910a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1807,7 +1807,7 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not Image.core.libtiff_support_custom_tags: + if not getattr(Image.core, "libtiff_support_custom_tags", False): continue if tag in ifd.tagtype: From 16d235f8bc5055a961bfcad1006218171f305510 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:08:19 +0000 Subject: [PATCH 144/155] Fix order of arguments in docstring --- docs/reference/ImageDraw.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9565ab149..43a5a2bc2 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -320,8 +320,8 @@ Methods :param xy: Two points to define the bounding box. Sequence of either ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. .. versionadded:: 5.3.0 @@ -334,8 +334,8 @@ Methods ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. From f03f9670fa986bba4c765440c129220bd5f1e006 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Sat, 18 Mar 2023 14:12:53 +0000 Subject: [PATCH 145/155] Removed incorrect docstring --- src/PIL/ImageDraw.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 5a0df09cb..4513d514f 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -423,7 +423,6 @@ class ImageDraw: self.draw.draw_rectangle(right, ink, 1) def _multiline_check(self, text): - """Draw text.""" split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text From fd3da53b91f5d4d1dbf552b614a05fde789c3ed0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 06:46:22 +1100 Subject: [PATCH 146/155] Updated xz to 5.4.2 --- 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 2820bdb36..f363b9454 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -152,9 +152,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.1.tar.gz/download", - "filename": "xz-5.4.1.tar.gz", - "dir": "xz-5.4.1", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.2.tar.gz/download", + "filename": "xz-5.4.2.tar.gz", + "dir": "xz-5.4.2", "license": "COPYING", "patch": { r"src\liblzma\api\lzma.h": { From fec92ce9cc214c12b66881e4da451993375903a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 19 Mar 2023 13:36:43 +1100 Subject: [PATCH 147/155] Restored deleted docstring --- src/PIL/ImageDraw.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 4513d514f..8adcc87de 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -463,6 +463,7 @@ class ImageDraw: *args, **kwargs, ): + """Draw text.""" if self._multiline_check(text): return self.multiline_text( xy, From c3a7422ad3c579c98f87d5dcd5d073fd6dda618e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 22:36:37 +1100 Subject: [PATCH 148/155] Added Amazon Linux 2023 docker image --- .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 ff1605ac7..14592ea1d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -33,6 +33,7 @@ jobs: # Then run the remainder alpine, amazon-2-amd64, + amazon-2023-amd64, arch, centos-7-amd64, centos-stream-8-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 6164a638b..cb2e4a74a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -424,6 +424,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2 | 3.7 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2023 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Arch | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS 7 | 3.9 | x86-64 | From 63286622488ffaeb481f74a0c499fd8732df98e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 17:34:34 +1100 Subject: [PATCH 149/155] PyUnicode_* may return NULL --- src/_imaging.c | 14 +++++++------- src/_imagingcms.c | 2 +- src/_imagingft.c | 4 ++-- src/_imagingmorph.c | 2 +- src/_webp.c | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 847eed5ce..96bfe5df7 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4213,7 +4213,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpegVersion(void); PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); - Py_DECREF(jpeglib_version); + Py_XDECREF(jpeglib_version); } #endif @@ -4222,7 +4222,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpeg2KVersion(void); PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); - Py_DECREF(jp2klib_version); + Py_XDECREF(jp2klib_version); } #endif @@ -4233,7 +4233,7 @@ setup_module(PyObject *m) { #define tostr(a) tostr1(a) PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); - Py_DECREF(libjpeg_turbo_version); + Py_XDECREF(libjpeg_turbo_version); #undef tostr #undef tostr1 #else @@ -4249,7 +4249,7 @@ setup_module(PyObject *m) { extern const char *ImagingImageQuantVersion(void); PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); PyDict_SetItemString(d, "imagequant_version", imagequant_version); - Py_DECREF(imagequant_version); + Py_XDECREF(imagequant_version); } #else have_libimagequant = Py_False; @@ -4268,7 +4268,7 @@ setup_module(PyObject *m) { extern const char *ImagingZipVersion(void); PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); PyDict_SetItemString(d, "zlib_version", zlibversion); - Py_DECREF(zlibversion); + Py_XDECREF(zlibversion); } #endif @@ -4277,7 +4277,7 @@ setup_module(PyObject *m) { extern const char *ImagingTiffVersion(void); PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", libtiff_version); - Py_DECREF(libtiff_version); + Py_XDECREF(libtiff_version); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4302,7 +4302,7 @@ setup_module(PyObject *m) { PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); - Py_DECREF(pillow_version); + Py_XDECREF(pillow_version); return 0; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 779f31b9c..521151ae0 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1534,7 +1534,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } PyDict_SetItemString(d, "littlecms_version", v); - Py_DECREF(v); + Py_XDECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 8697a74ff..beab7e0bd 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1366,7 +1366,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); PyDict_SetItemString(d, "freetype2_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1392,7 +1392,7 @@ setup_module(PyObject *m) { v = Py_None; #endif PyDict_SetItemString(d, "raqm_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef FRIBIDI_MAJOR_VERSION { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 46a40e96d..7f85bc096 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -244,7 +244,7 @@ setup_module(PyObject *m) { PyObject *version = PyUnicode_FromString("0.1"); PyDict_SetItemString(d, "__version", version); - Py_DECREF(version); + Py_XDECREF(version); return 0; } diff --git a/src/_webp.c b/src/_webp.c index 5575e04f9..5c86341fa 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -964,7 +964,7 @@ setup_module(PyObject *m) { PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); - Py_DECREF(webpdecoder_version); + Py_XDECREF(webpdecoder_version); #ifdef HAVE_WEBPANIM /* Ready object types */ From 76d36da12e8b17d7e91814056c88bea5b0c614fa Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 00:59:00 +0000 Subject: [PATCH 150/155] avoid Py_DECREF(Py_None) --- src/_imagingft.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index beab7e0bd..dfb3697e2 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1365,7 +1365,7 @@ setup_module(PyObject *m) { FT_Library_Version(library, &major, &minor, &patch); v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); - PyDict_SetItemString(d, "freetype2_version", v); + PyDict_SetItemString(d, "freetype2_version", v ? v : Py_None); Py_XDECREF(v); #ifdef HAVE_RAQM @@ -1386,35 +1386,32 @@ setup_module(PyObject *m) { PyDict_SetItemString(d, "HAVE_HARFBUZZ", v); Py_DECREF(v); if (have_raqm) { + v = NULL; #ifdef RAQM_VERSION_MAJOR v = PyUnicode_FromString(raqm_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "raqm_version", v); + PyDict_SetItemString(d, "raqm_version", v ? v : Py_None); Py_XDECREF(v); + v = NULL; #ifdef FRIBIDI_MAJOR_VERSION { const char *a = strchr(fribidi_version_info, ')'); const char *b = strchr(fribidi_version_info, '\n'); if (a && b && a + 2 < b) { v = PyUnicode_FromStringAndSize(a + 2, b - (a + 2)); - } else { - v = Py_None; } } -#else - v = Py_None; #endif - PyDict_SetItemString(d, "fribidi_version", v); + PyDict_SetItemString(d, "fribidi_version", v ? v : Py_None); + Py_XDECREF(v); + v = NULL; #ifdef HB_VERSION_STRING v = PyUnicode_FromString(hb_version_string()); -#else - v = Py_None; #endif - PyDict_SetItemString(d, "harfbuzz_version", v); + PyDict_SetItemString(d, "harfbuzz_version", v ? v : Py_None); + Py_XDECREF(v); } return 0; From b3bf1ca6d9ca1d8f6ab3118448cfc4e27984e0f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 21 Mar 2023 22:36:36 +1100 Subject: [PATCH 151/155] Fixed calling html target from htmlview --- docs/make.bat | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/make.bat b/docs/make.bat index ad720cdcd..0ed5ee1a5 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -45,20 +45,20 @@ if "%1" == "clean" ( goto end ) -if "%1" == "html" ( +set html=false +if "%1%" == "html" set html=true +if "%1%" == "htmlview" set html=true +if "%html%" == "true" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) -if "%1" == "htmlview" ( - cmd /C %this% html - - if EXIST "%BUILDDIR%\html\index.html" ( - echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... - start "" "%BUILDDIR%\html\index.html" + if "%1" == "htmlview" ( + if EXIST "%BUILDDIR%\html\index.html" ( + echo.Opening "%BUILDDIR%\html\index.html" in the default web browser... + start "" "%BUILDDIR%\html\index.html" + ) ) goto end From 3a262f0523cb2aea44a1678a282ad2d0ec9014c3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 21 Mar 2023 14:05:58 +0200 Subject: [PATCH 152/155] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Makefile | 4 ++-- docs/conf.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 1691e4657..bb0ea60b3 100644 --- a/Makefile +++ b/Makefile @@ -44,8 +44,8 @@ help: @echo " coverage run coverage test (in progress)" @echo " doc make HTML docs" @echo " docserve run an HTTP server on the docs directory" - @echo " html to make standalone HTML files" - @echo " htmlview to open the index page built by the html target in your browser" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" @echo " inplace make inplace extension" @echo " install make and install" @echo " install-coverage make and install with C coverage" diff --git a/docs/conf.py b/docs/conf.py index 683ff7856..2ebcd6b2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,8 +28,8 @@ needs_sphinx = "2.4" # ones. extensions = [ "sphinx.ext.autodoc", - "sphinx.ext.intersphinx", "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_inline_tabs", From 1a11ba662c244bbb6ab3b7c3753eea8887d3c99f Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 21:44:52 +0000 Subject: [PATCH 153/155] do not insert null into dict --- src/_imaging.c | 77 ++++++++++++++++++++++++----------------------- src/_imagingcms.c | 2 +- src/_imagingft.c | 8 ++--- src/_webp.c | 6 ++-- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 96bfe5df7..2229235db 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3810,6 +3810,7 @@ static PyTypeObject PixelAccess_Type = { static PyObject * _get_stats(PyObject *self, PyObject *args) { PyObject *d; + PyObject *v; ImagingMemoryArena arena = &ImagingDefaultArena; if (!PyArg_ParseTuple(args, ":get_stats")) { @@ -3820,29 +3821,29 @@ _get_stats(PyObject *self, PyObject *args) { if (!d) { return NULL; } - PyObject *new_count = PyLong_FromLong(arena->stats_new_count); - PyDict_SetItemString(d, "new_count", new_count); - Py_XDECREF(new_count); + v = PyLong_FromLong(arena->stats_new_count); + PyDict_SetItemString(d, "new_count", v ? v : Py_None); + Py_XDECREF(v); - PyObject *allocated_blocks = PyLong_FromLong(arena->stats_allocated_blocks); - PyDict_SetItemString(d, "allocated_blocks", allocated_blocks); - Py_XDECREF(allocated_blocks); + v = PyLong_FromLong(arena->stats_allocated_blocks); + PyDict_SetItemString(d, "allocated_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *reused_blocks = PyLong_FromLong(arena->stats_reused_blocks); - PyDict_SetItemString(d, "reused_blocks", reused_blocks); - Py_XDECREF(reused_blocks); + v = PyLong_FromLong(arena->stats_reused_blocks); + PyDict_SetItemString(d, "reused_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *reallocated_blocks = PyLong_FromLong(arena->stats_reallocated_blocks); - PyDict_SetItemString(d, "reallocated_blocks", reallocated_blocks); - Py_XDECREF(reallocated_blocks); + v = PyLong_FromLong(arena->stats_reallocated_blocks); + PyDict_SetItemString(d, "reallocated_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *freed_blocks = PyLong_FromLong(arena->stats_freed_blocks); - PyDict_SetItemString(d, "freed_blocks", freed_blocks); - Py_XDECREF(freed_blocks); + v = PyLong_FromLong(arena->stats_freed_blocks); + PyDict_SetItemString(d, "freed_blocks", v ? v : Py_None); + Py_XDECREF(v); - PyObject *blocks_cached = PyLong_FromLong(arena->blocks_cached); - PyDict_SetItemString(d, "blocks_cached", blocks_cached); - Py_XDECREF(blocks_cached); + v = PyLong_FromLong(arena->blocks_cached); + PyDict_SetItemString(d, "blocks_cached", v ? v : Py_None); + Py_XDECREF(v); return d; } @@ -4211,31 +4212,33 @@ setup_module(PyObject *m) { #ifdef HAVE_LIBJPEG { extern const char *ImagingJpegVersion(void); - PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); - PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); - Py_XDECREF(jpeglib_version); + PyObject *v = PyUnicode_FromString(ImagingJpegVersion()); + PyDict_SetItemString(d, "jpeglib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_OPENJPEG { extern const char *ImagingJpeg2KVersion(void); - PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); - PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); - Py_XDECREF(jp2klib_version); + PyObject *v = PyUnicode_FromString(ImagingJpeg2KVersion()); + PyDict_SetItemString(d, "jp2klib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif PyObject *have_libjpegturbo; #ifdef LIBJPEG_TURBO_VERSION have_libjpegturbo = Py_True; + { #define tostr1(a) #a #define tostr(a) tostr1(a) - PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); - PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); - Py_XDECREF(libjpeg_turbo_version); + PyObject *v = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); + PyDict_SetItemString(d, "libjpeg_turbo_version", v ? v : Py_None); + Py_XDECREF(v); #undef tostr #undef tostr1 + } #else have_libjpegturbo = Py_False; #endif @@ -4247,9 +4250,9 @@ setup_module(PyObject *m) { have_libimagequant = Py_True; { extern const char *ImagingImageQuantVersion(void); - PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); - PyDict_SetItemString(d, "imagequant_version", imagequant_version); - Py_XDECREF(imagequant_version); + PyObject *v = PyUnicode_FromString(ImagingImageQuantVersion()); + PyDict_SetItemString(d, "imagequant_version", v ? v : Py_None); + Py_XDECREF(v); } #else have_libimagequant = Py_False; @@ -4266,18 +4269,18 @@ setup_module(PyObject *m) { PyModule_AddIntConstant(m, "FIXED", Z_FIXED); { extern const char *ImagingZipVersion(void); - PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); - PyDict_SetItemString(d, "zlib_version", zlibversion); - Py_XDECREF(zlibversion); + PyObject *v = PyUnicode_FromString(ImagingZipVersion()); + PyDict_SetItemString(d, "zlib_version", v ? v : Py_None); + Py_XDECREF(v); } #endif #ifdef HAVE_LIBTIFF { extern const char *ImagingTiffVersion(void); - PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); - PyDict_SetItemString(d, "libtiff_version", libtiff_version); - Py_XDECREF(libtiff_version); + PyObject *v = PyUnicode_FromString(ImagingTiffVersion()); + PyDict_SetItemString(d, "libtiff_version", v ? v : Py_None); + Py_XDECREF(v); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4301,7 +4304,7 @@ setup_module(PyObject *m) { PyModule_AddObject(m, "HAVE_XCB", have_xcb); PyObject *pillow_version = PyUnicode_FromString(version); - PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); + PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version ? pillow_version : Py_None); Py_XDECREF(pillow_version); return 0; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 521151ae0..ddfe6ad64 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1533,7 +1533,7 @@ setup_module(PyObject *m) { } else { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } - PyDict_SetItemString(d, "littlecms_version", v); + PyDict_SetItemString(d, "littlecms_version", v ? v : Py_None); Py_XDECREF(v); return 0; diff --git a/src/_imagingft.c b/src/_imagingft.c index dfb3697e2..4f44d6a71 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1130,15 +1130,15 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); - PyDict_SetItemString(list_axis, "minimum", minimum); + PyDict_SetItemString(list_axis, "minimum", minimum ? minimum : Py_None); Py_XDECREF(minimum); PyObject *def = PyLong_FromLong(axis.def / 65536); - PyDict_SetItemString(list_axis, "default", def); + PyDict_SetItemString(list_axis, "default", def ? def : Py_None); Py_XDECREF(def); PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); - PyDict_SetItemString(list_axis, "maximum", maximum); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); Py_XDECREF(maximum); for (j = 0; j < name_count; j++) { @@ -1149,7 +1149,7 @@ font_getvaraxes(FontObject *self) { if (name.name_id == axis.strid) { axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); Py_XDECREF(axis_name); break; } diff --git a/src/_webp.c b/src/_webp.c index 5c86341fa..31055fecd 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -962,9 +962,9 @@ setup_module(PyObject *m) { addAnimFlagToModule(m); addTransparencyFlagToModule(m); - PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); - PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); - Py_XDECREF(webpdecoder_version); + PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); + PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); + Py_XDECREF(v); #ifdef HAVE_WEBPANIM /* Ready object types */ From e1d0a96404fb98ddbbcba5d7a04f1e836adccdf8 Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 21:46:33 +0000 Subject: [PATCH 154/155] remove unused version value --- src/_imagingmorph.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 7f85bc096..2e3545863 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -238,17 +238,6 @@ get_on_pixels(PyObject *self, PyObject *args) { return ret; } -static int -setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); - - PyObject *version = PyUnicode_FromString("0.1"); - PyDict_SetItemString(d, "__version", version); - Py_XDECREF(version); - - return 0; -} - static PyMethodDef functions[] = { /* Functions */ {"apply", (PyCFunction)apply, METH_VARARGS, NULL}, @@ -270,9 +259,5 @@ PyInit__imagingmorph(void) { m = PyModule_Create(&module_def); - if (setup_module(m) < 0) { - return NULL; - } - return m; } From adae44da0dd5c6f7ce50701fecec39ae1a9ac88d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 22 Mar 2023 09:10:47 +1100 Subject: [PATCH 155/155] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d7b5b4dab..706d9ad76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Allow libtiff_support_custom_tags to be missing #7020 + [radarhere] + - Improved I;16N support #6834 [radarhere]