From 700a8e98da37cb586e15fffeb46c04d755950f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:16:44 +1100 Subject: [PATCH 001/512] 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/512] 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/512] 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/512] 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 ea83ebbcf90ffff7b68ffc9ce90aad5c14c10f05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 6 Jan 2023 20:09:47 +1100 Subject: [PATCH 005/512] 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 c3134dc04994fa3125f127c6c5a54f03e294fa5e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 10 Jan 2023 00:03:07 -0600 Subject: [PATCH 006/512] refactor EpsImagePlugin Merge the PSFile class into the EpsImageFile class to hopefully improve performance. Also added a check for the required "%!PS-Adobe" and "%%BoundingBox" header comments. --- Tests/test_file_eps.py | 27 ++++++++- src/PIL/EpsImagePlugin.py | 113 +++++++++++++++++++++++--------------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 015dda992..9558d149f 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -221,7 +221,7 @@ def test_read_binary_preview(): pass -def test_readline(tmp_path): +def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' line_endings = ["\r\n", "\n", "\n\r", "\r"] @@ -256,6 +256,31 @@ def test_readline(tmp_path): _test_readline_file_psfile(s, ending) +@pytest.mark.parametrize( + "line_ending", + (b"\r\n", b"\n", b"\n\r", b"\r"), +) +def test_readline(line_ending): + simple_file = line_ending.join( + ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", + ) + ) + + data = io.BytesIO(simple_file) + test_file = EpsImagePlugin.EpsImageFile(data) + assert test_file.info["Comment1"] == "Some Value" + assert test_file.info["SecondComment"] == "Another Value" + assert test_file.size == (100, 100) + + @pytest.mark.parametrize( "filename", ( diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 016e3c135..6b3e353f2 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -162,6 +162,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. + This class is no longer used internally, but kept for backwards-compatibility. """ def __init__(self, fp): @@ -194,7 +195,7 @@ def _accept(prefix): ## -# Image plugin for Encapsulated PostScript. This plugin supports only +# Image plugin for Encapsulated PostScript. This plugin supports only # a few variants of this format. @@ -209,29 +210,69 @@ class EpsImageFile(ImageFile.ImageFile): def _open(self): (length, offset) = self._find_offset(self.fp) - # Rewrap the open file pointer in something that will - # convert line endings and decode to latin-1. - fp = PSFile(self.fp) - # go to offset - start of "%!PS" - fp.seek(offset) - - box = None + self.fp.seek(offset) self.mode = "RGB" - self._size = 1, 1 # FIXME: huh? + self._size = None - # - # Load EPS header + byte_arr = bytearray(255) + bytes_mv = memoryview(byte_arr) + bytes_read = 0 + reading_comments = True - s_raw = fp.readline() - s = s_raw.strip("\r\n") + def check_required_header_comments(): + if "PS-Adobe" not in self.info: + msg = 'EPS header missing "%!PS-Adobe" comment' + raise SyntaxError(msg) + if "BoundingBox" not in self.info: + msg = 'EPS header missing "%%BoundingBox" comment' + raise SyntaxError(msg) - while s_raw: - if s: - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) + while True: + byte = self.fp.read(1) + if byte == b"": + # if we didn't read a byte we must be at the end of the file + if bytes_read == 0: + break + elif byte in b"\r\n": + # if we read a line ending character, ignore it and parse what + # we have already read. if we haven't read any other characters, + # continue reading + if bytes_read == 0: + continue + else: + # ASCII/hexadecimal lines in an EPS file must not exceed + # 255 characters, not including line ending characters + if bytes_read >= 255: + # only enforce this for lines starting with a "%", + # otherwise assume it's binary data + if byte_arr[0] == ord("%"): + msg = "not an EPS file" + raise SyntaxError(msg) + else: + if reading_comments: + check_required_header_comments() + reading_comments = False + # reset bytes_read so we can keep reading + # data until the end of the line + bytes_read = 0 + byte_arr[bytes_read] = byte[0] + bytes_read += 1 + continue + + if reading_comments: + # Load EPS header + + # if this line doesn't start with a "%", + # or does start with "%%EndComments", + # then we've reached the end of the header/comments + if byte_arr[0] != ord("%") or bytes_mv[:13] == b"%%EndComments": + check_required_header_comments() + reading_comments = False + continue + + s = str(bytes_mv[:bytes_read], "latin-1") try: m = split.match(s) @@ -254,16 +295,12 @@ class EpsImageFile(ImageFile.ImageFile): ] except Exception: pass - else: m = field.match(s) if m: k = m.group(1) - - if k == "EndComments": - break if k[:8] == "PS-Adobe": - self.info[k[:8]] = k[9:] + self.info["PS-Adobe"] = k[9:] else: self.info[k] = "" elif s[0] == "%": @@ -273,25 +310,11 @@ class EpsImageFile(ImageFile.ImageFile): else: msg = "bad EPS header" raise OSError(msg) + elif bytes_mv[:11] == b"%ImageData:": + # Check for an "ImageData" descriptor - s_raw = fp.readline() - s = s_raw.strip("\r\n") - - if s and s[:1] != "%": - break - - # - # Scan for an "ImageData" descriptor - - while s[:1] == "%": - - if len(s) > 255: - msg = "not an EPS file" - raise SyntaxError(msg) - - if s[:11] == "%ImageData:": # Encoded bitmapped image. - x, y, bi, mo = s[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" @@ -306,16 +329,16 @@ class EpsImageFile(ImageFile.ImageFile): self._size = int(x), int(y) return - s = fp.readline().strip("\r\n") - if not s: - break + bytes_read = 0 - if not box: + check_required_header_comments() + + if not self._size: + self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) def _find_offset(self, fp): - s = fp.read(160) if s[:4] == b"%!PS": From 0334e68f956a18350fc2cba872aef54e57c51e14 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 12 Jan 2023 08:36:17 -0600 Subject: [PATCH 007/512] add more eps file tests --- Tests/test_file_eps.py | 109 ++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 9558d149f..bded99cf1 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -28,34 +28,47 @@ FILE2_COMPARE_SCALE2 = "Tests/images/non_zero_bb_scale2.png" # EPS test files with binary preview FILE3 = "Tests/images/binary_preview_map.eps" +# Three unsigned 32bit little-endian values: +# 0xC6D3D0C5 magic number +# byte position of start of postscript section (12) +# byte length of postscript section (0) +# this byte length isn't valid, but we don't read it +simple_binary_header = b"\xc5\xd0\xd3\xc6\x0c\x00\x00\x00\x00\x00\x00\x00" + +# taken from page 8 of the specification +# https://web.archive.org/web/20220120164601/https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/5002.EPSF_Spec.pdf +simple_eps_file = ( + b"%!PS-Adobe-3.0 EPSF-3.0", + b"%%BoundingBox: 5 5 105 105", + b"10 setlinewidth", + b"10 10 moveto", + b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", + b"stroke", +) +simple_eps_file_with_comments = ( + simple_eps_file[:1] + + ( + b"%%Comment1: Some Value", + b"%%SecondComment: Another Value", + ) + + simple_eps_file[1:] +) +simple_eps_file_without_version = simple_eps_file[1:] +simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] + @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") -def test_sanity(): - # Regular scale - with Image.open(FILE1) as image1: - image1.load() - assert image1.mode == "RGB" - assert image1.size == (460, 352) - assert image1.format == "EPS" - - with Image.open(FILE2) as image2: - image2.load() - assert image2.mode == "RGB" - assert image2.size == (360, 252) - assert image2.format == "EPS" - - # Double scale - with Image.open(FILE1) as image1_scale2: - image1_scale2.load(scale=2) - assert image1_scale2.mode == "RGB" - assert image1_scale2.size == (920, 704) - assert image1_scale2.format == "EPS" - - with Image.open(FILE2) as image2_scale2: - image2_scale2.load(scale=2) - assert image2_scale2.mode == "RGB" - assert image2_scale2.size == (720, 504) - assert image2_scale2.format == "EPS" +@pytest.mark.parametrize( + ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) +) +@pytest.mark.parametrize("scale", (1, 2)) +def test_sanity(filename, size, scale): + expected_size = tuple(s * scale for s in size) + with Image.open(filename) as image: + image.load(scale=scale) + assert image.mode == "RGB" + assert image.size == expected_size + assert image.format == "EPS" @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -69,18 +82,36 @@ def test_load(): def test_invalid_file(): invalid_file = "Tests/images/flower.jpg" - with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(invalid_file) +def test_binary_header_only(): + data = io.BytesIO(simple_binary_header) + with pytest.raises(SyntaxError, match='EPS header missing "%!PS-Adobe" comment'): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_version_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) + with pytest.raises(SyntaxError): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_missing_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) + with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): + EpsImagePlugin.EpsImageFile(data) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) @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" @@ -101,7 +132,7 @@ def test_showpage(): with Image.open("Tests/images/reqd_showpage.png") as target: # should not crash/hang plot_image.load() - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -112,7 +143,7 @@ def test_transparency(): assert plot_image.mode == "RGBA" with Image.open("Tests/images/reqd_showpage_transparency.png") as target: - # fonts could be slightly different + # fonts could be slightly different assert_image_similar(plot_image, target, 6) @@ -207,7 +238,6 @@ def test_resize(filename): @pytest.mark.parametrize("filename", (FILE1, FILE2)) def test_thumbnail(filename): # Issue #619 - # Arrange with Image.open(filename) as im: new_size = (100, 100) im.thumbnail(new_size) @@ -256,24 +286,13 @@ def test_readline_psfile(tmp_path): _test_readline_file_psfile(s, ending) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize( "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(line_ending): - simple_file = line_ending.join( - ( - b"%!PS-Adobe-3.0 EPSF-3.0", - b"%%Comment1: Some Value", - b"%%SecondComment: Another Value", - b"%%BoundingBox: 5 5 105 105", - b"10 setlinewidth", - b"10 10 moveto", - b"0 90 rlineto 90 0 rlineto 0 -90 rlineto closepath", - b"stroke", - ) - ) - +def test_readline(prefix, line_ending): + simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) assert test_file.info["Comment1"] == "Some Value" From 4c2550db423523efedd773a3042754f2ad627477 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:29:23 -0600 Subject: [PATCH 008/512] add test for invalid bounding box --- Tests/test_file_eps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index bded99cf1..7abed6f42 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,6 +55,7 @@ simple_eps_file_with_comments = ( ) simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -106,6 +107,13 @@ def test_missing_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) + with pytest.raises(OSError, match="cannot determine EPS bounding box"): + EpsImagePlugin.EpsImageFile(data) + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) From 3d6770d0f33fdfc18a6833a384fbccb9ef878dfa Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 15:56:25 -0600 Subject: [PATCH 009/512] add tests for long lines --- Tests/test_file_eps.py | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 7abed6f42..26ac2e5a1 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -55,7 +55,21 @@ simple_eps_file_with_comments = ( ) simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] -simple_eps_file_with_invalid_boundingbox = simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] +simple_eps_file_with_invalid_boundingbox = ( + simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] +) +simple_eps_file_with_long_ascii_comment = ( + simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] +) +simple_eps_file_with_long_binary_data = ( + simple_eps_file[:2] + + ( + b"%%BeginBinary: 300", + b"\0" * 300, + b"%%EndBinary", + ) + + simple_eps_file[2:] +) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @@ -114,6 +128,30 @@ def test_invalid_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_ascii_comment_too_long(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) + with pytest.raises(SyntaxError, match="not an EPS file"): + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + EpsImagePlugin.EpsImageFile(data) + + +@pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_load_long_binary_data(prefix): + data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) + with Image.open(data) as img: + img.load() + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @mark_if_feature_version( pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) From 7ad50d9185a7beef755ea7c19b8cfe6e5bea815e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:42:55 -0600 Subject: [PATCH 010/512] 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 e3dd4de1934c35144430de1a411d1df5bad84732 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 20:08:37 -0600 Subject: [PATCH 011/512] parametrize check_jpeg_leaks::test_qtables_leak() Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_jpeg_leaks.py | 71 +++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..940c0b00d 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -75,43 +75,42 @@ post-patch: """ -def test_qtables_leak(): +standard_l_qtable = ( + # fmt: off + 16, 11, 10, 16, 24, 40, 51, 61, + 12, 12, 14, 19, 26, 58, 60, 55, + 14, 13, 16, 24, 40, 57, 69, 56, + 14, 17, 22, 29, 51, 87, 80, 62, + 18, 22, 37, 56, 68, 109, 103, 77, + 24, 35, 55, 64, 81, 104, 113, 92, + 49, 64, 78, 87, 103, 121, 120, 101, + 72, 92, 95, 98, 112, 100, 103, 99, + # fmt: on +) + +standard_chrominance_qtable = ( + # fmt: off + 17, 18, 24, 47, 99, 99, 99, 99, + 18, 21, 26, 66, 99, 99, 99, 99, + 24, 26, 56, 99, 99, 99, 99, 99, + 47, 66, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, + # fmt: on +) + + +@pytest.mark.parametrize( + "qtables", + ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ), +) +def test_qtables_leak(qtables): im = hopper("RGB") - - standard_l_qtable = [ - int(s) - for s in """ - 16 11 10 16 24 40 51 61 - 12 12 14 19 26 58 60 55 - 14 13 16 24 40 57 69 56 - 14 17 22 29 51 87 80 62 - 18 22 37 56 68 109 103 77 - 24 35 55 64 81 104 113 92 - 49 64 78 87 103 121 120 101 - 72 92 95 98 112 100 103 99 - """.split( - None - ) - ] - - standard_chrominance_qtable = [ - int(s) - for s in """ - 17 18 24 47 99 99 99 99 - 18 21 26 66 99 99 99 99 - 24 26 56 99 99 99 99 99 - 47 66 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - 99 99 99 99 99 99 99 99 - """.split( - None - ) - ] - - qtables = [standard_l_qtable, standard_chrominance_qtable] - for _ in range(iterations): test_output = BytesIO() im.save(test_output, "JPEG", qtables=qtables) From 1603872f244da741feeac5a87d235e570723853f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 16 Jan 2023 07:46:11 -0600 Subject: [PATCH 012/512] 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 013/512] 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 fcf5b7ef8319730df1de5df76cdebe427ae4a4c3 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:33:15 +1100 Subject: [PATCH 014/512] Fixed merge conflicts during recent pull --- src/encode.c | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/encode.c b/src/encode.c index 21c42d915..33a2a37a7 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,10 +1214,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; + char * comment = NULL; + int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbn", + "ss|OOOsOnOOOssbbnzp", &mode, &format, &offset, @@ -1233,7 +1235,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &cinema_mode, &mct, &sgnd, - &fd)) { + &fd, + &comment, + &add_plt)) { return NULL; } @@ -1315,6 +1319,29 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } + if (comment != NULL && strlen(comment) > 0) { + /* Size is stored as as an uint16, subtract 4 bytes for the header */ + if (strlen(comment) >= 65531) { + PyErr_SetString( + PyExc_ValueError, + "JPEG 2000 comment is too long"); + Py_DECREF(encoder); + return NULL; + } + + context->comment = strdup(comment); + + if (context->comment == NULL) { + PyErr_SetString( + PyExc_MemoryError, + "Couldn't allocate memory for JPEG 2000 comment"); + Py_DECREF(encoder); + return NULL; + } + } else { + context->comment = NULL; + } + if (quality_layers && PySequence_Check(quality_layers)) { context->quality_is_in_db = strcmp(quality_mode, "dB") == 0; context->quality_layers = quality_layers; @@ -1332,6 +1359,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; + context->add_plt = add_plt; return (PyObject *)encoder; } From de43bc99c873fe0c752f5e303c4a40f954b61912 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 11:37:14 +1100 Subject: [PATCH 015/512] Added support for jpeg2000 comments and PLT marker segments --- Tests/test_file_jpeg2k.py | 44 ++++++++++++++++++++++++++++ docs/handbook/image-file-formats.rst | 13 ++++++++ src/PIL/Jpeg2KImagePlugin.py | 4 +++ src/libImaging/Jpeg2K.h | 6 ++++ src/libImaging/Jpeg2KEncode.c | 17 +++++++++++ 5 files changed, 84 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 0229b2243..f52c33402 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,5 +1,6 @@ import os import re +import struct from io import BytesIO import pytest @@ -371,3 +372,46 @@ def test_crashes(test_file): im.load() except OSError: pass + + +def test_custom_comment(): + output_stream = BytesIO() + unique_comment = "This is a unique comment, which should be found below" + test_card.save(output_stream, "JPEG2000", comment=unique_comment) + output_stream.seek(0) + data = output_stream.read() + # Lazy method to determine if the comment is in the image generated + assert(bytes(unique_comment, "utf-8") in data) + + +def test_plt_marker(): + # Search the start of the codesteam for the PLT box (id 0xFF58) + opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) + assert opj_version is not None + + if float(opj_version[1]) >= 2.4: + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encounterd and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] + + if jp2_boxid == 0xff4f: + # No length specifier for main header + continue + elif jp2_boxid == 0xff58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xff93: + break + # SOD box encountered and no PLT, so it wasn't found + + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) + + # The PLT box wasn't found + raise ValueError diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a41ef7cf8..9128400ac 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -589,6 +589,19 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 +**comment** + Adds a custom comment to the file, replacing the default + "Created by OpenJPEG version" comment. + + .. versionadded:: 9.5.0 + +**add_plt** + If ``True`` then include a PLT (packet length, tile-part header) marker + segment in the produced file. + The default is to not include it. + + .. versionadded:: 9.5.0 + .. note:: To enable JPEG 2000 support, you need to build and install the OpenJPEG diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 7457874c1..754010c7c 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,6 +328,8 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 + comment = info.get("comment", None) + add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): try: @@ -350,6 +352,8 @@ def _save(im, fp, filename): mct, signed, fd, + comment, + add_plt ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index b28a0440a..65728be5d 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -97,6 +97,12 @@ typedef struct { /* PRIVATE CONTEXT (set by decoder) */ const char *error_msg; + /* Custom comment */ + char * comment; + + /* Include PLT marker segment */ + int add_plt; + } JPEG2KENCODESTATE; /* diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index db1c5c0c9..bb280ae94 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -439,6 +439,10 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { params.tcp_mct = context->mct; } + if (context->comment) { + params.cp_comment = context->comment; + } + params.prog_order = context->progression; params.cp_cinema = context->cinema_mode; @@ -492,6 +496,14 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { opj_set_warning_handler(codec, j2k_warn, context); opj_setup_encoder(codec, ¶ms, image); + /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ +#if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) + if (context->add_plt) { + const char * plt_option[2] = {"PLT=YES", NULL}; + opj_encoder_set_extra_options(codec, plt_option); + } +#endif + /* Start encoding */ if (!opj_start_compress(codec, image, stream)) { state->errcode = IMAGING_CODEC_BROKEN; @@ -624,7 +636,12 @@ ImagingJpeg2KEncodeCleanup(ImagingCodecState state) { free((void *)context->error_msg); } + if (context->comment) { + free((void *)context->comment); + } + context->error_msg = NULL; + context->comment = NULL; return -1; } From 41b3ac8aed826679485913f4e1c94320408016e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Jan 2023 00:52:38 +0000 Subject: [PATCH 016/512] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_jpeg2k.py | 8 ++++---- src/PIL/Jpeg2KImagePlugin.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index f52c33402..6107075d5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -381,7 +381,7 @@ def test_custom_comment(): output_stream.seek(0) data = output_stream.read() # Lazy method to determine if the comment is in the image generated - assert(bytes(unique_comment, "utf-8") in data) + assert bytes(unique_comment, "utf-8") in data def test_plt_marker(): @@ -400,13 +400,13 @@ def test_plt_marker(): break jp2_boxid = struct.unpack(">H", box_bytes)[0] - if jp2_boxid == 0xff4f: + if jp2_boxid == 0xFF4F: # No length specifier for main header continue - elif jp2_boxid == 0xff58: + elif jp2_boxid == 0xFF58: # This is the PLT box we're looking for return - elif jp2_boxid == 0xff93: + elif jp2_boxid == 0xFF93: break # SOD box encountered and no PLT, so it wasn't found diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 754010c7c..001dcf39c 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -353,7 +353,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt + add_plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) From d55563ca2519765473f87271f4f39ebd75a9150e Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 12:05:05 +1100 Subject: [PATCH 017/512] Update docs/handbook/image-file-formats.rst to fix lint Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/handbook/image-file-formats.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 9128400ac..f466ccac8 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -590,13 +590,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.1.0 **comment** - Adds a custom comment to the file, replacing the default + Adds a custom comment to the file, replacing the default "Created by OpenJPEG version" comment. .. versionadded:: 9.5.0 **add_plt** - If ``True`` then include a PLT (packet length, tile-part header) marker + If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From e8307d74064d555524558a1dcb8828006ab3d65a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 18 Jan 2023 23:03:13 -0600 Subject: [PATCH 018/512] more imagepath tests --- Tests/test_imagepath.py | 83 +++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 861fb64f0..2b378d333 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 @@ -39,48 +38,76 @@ def test_path(): p.transform((1, 0, 1, 0, 1, 1)) assert list(p) == [(1.0, 2.0), (5.0, 6.0), (9.0, 10.0)] - # alternative constructors - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0.0, 1.0]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([0, 1]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path([(0, 1)]) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(0)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(p.tolist(1)) - assert list(p) == [(0.0, 1.0)] - p = ImagePath.Path(array.array("f", [0, 1])) - assert list(p) == [(0.0, 1.0)] - arr = array.array("f", [0, 1]) - p = ImagePath.Path(arr.tobytes()) +@pytest.mark.parametrize( + "coords", + ( + (0, 1), + [0, 1], + (0.0, 1.0), + [0.0, 1.0], + ((0, 1),), + [(0, 1)], + ((0.0, 1.0),), + [(0.0, 1.0)], + array.array("f", [0, 1]), + array.array("f", [0.0, 1.0]), + ImagePath.Path((0, 1)), + ), +) +def test_path_constructors(coords): + # Arrange / Act + p = ImagePath.Path(coords) + + # Assert assert list(p) == [(0.0, 1.0)] -def test_invalid_coords(): +def test_path_constructor_text(): # Arrange - coords = ["a", "b"] + arr = array.array("f", (0, 1)) - # Act / Assert + # Act + p = ImagePath.Path(arr.tobytes()) + + # Assert + assert list(p) == [(0.0, 1.0)] + + +@pytest.mark.parametrize( + "coords", + ( + ("a", "b"), + ([0, 1],), + [[0, 1]], + ([0.0, 1.0],), + [[0.0, 1.0]], + ), +) +def test_invalid_path_constructors(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "incorrect coordinate type" -def test_path_odd_number_of_coordinates(): - # Arrange - coords = [0] - - # Act / Assert +@pytest.mark.parametrize( + "coords", + ( + (0,), + [0], + (0, 1, 2), + [0, 1, 2], + ), +) +def test_path_odd_number_of_coordinates(coords): + # Act with pytest.raises(ValueError) as e: ImagePath.Path(coords) + # Assert assert str(e.value) == "wrong number of coordinates" From b00bde977199968aa62c539b85ef3feb2338d080 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 22:52:41 +1100 Subject: [PATCH 019/512] Update Tests/test_file_jpeg2k.py fix spelling error Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/test_file_jpeg2k.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 6107075d5..ccc18772a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -396,7 +396,7 @@ def test_plt_marker(): while True: box_bytes = out.read(2) if len(box_bytes) == 0: - # End of steam encounterd and no PLT or SOD + # End of steam encountered and no PLT or SOD break jp2_boxid = struct.unpack(">H", box_bytes)[0] From 4e8de9ac9a1e14593e17a2cd1ff82c45ed0900cd Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 25 Jan 2023 08:13:40 -0600 Subject: [PATCH 020/512] add path-from-bytes test Also `array.array("f", [0, 1]) == array.array("f", [0.0, 1.0])` so we didn't need both of them. --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 2b378d333..7a517b6f6 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -51,7 +51,7 @@ def test_path(): ((0.0, 1.0),), [(0.0, 1.0)], array.array("f", [0, 1]), - array.array("f", [0.0, 1.0]), + array.array("f", [0, 1]).tobytes(), ImagePath.Path((0, 1)), ), ) From 9eacaee399acc4b61d420bd7edee6a83e03a7e07 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 09:36:22 +1100 Subject: [PATCH 021/512] Document how to create universal2 wheels --- docs/installation.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ea8722c56..28a00d1b1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -68,6 +68,18 @@ Install Pillow with :command:`pip`:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow + While we provide binaries for both x86-64 and arm64, we do not provide universal2 + binaries. However, it is simple to combine our current binaries to create one:: + + python3 -m pip download --only-binary=:all: --platform macosx_10_10_x86_64 Pillow + python3 -m pip download --only-binary=:all: --platform macosx_11_0_arm64 Pillow + python3 -m pip install delocate + + Then, with the names of the downloaded wheels, use Python to combine them:: + + from delocate.fuse import fuse_wheels + fuse_wheels('Pillow-9.4.0-2-cp39-cp39-macosx_10_10_x86_64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_arm64.whl', 'Pillow-9.4.0-cp39-cp39-macosx_11_0_universal2.whl') + .. tab:: Windows We provide Pillow binaries for Windows compiled for the matrix of From 73f55b4e01162c075f9955a5b3eaf86182ddebce Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 1 Jan 2023 19:32:21 +0100 Subject: [PATCH 022/512] 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 510de501ead4fab2d85e84f2f48ec98438dc0c9a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 29 Jan 2023 17:18:17 +1100 Subject: [PATCH 023/512] 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 024/512] 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 79e67cb5c33e5cef781e664439bc5c7371c1c04c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 31 Jan 2023 21:42:25 +1100 Subject: [PATCH 025/512] Removed default argument --- src/PIL/Jpeg2KImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 001dcf39c..68a354e3f 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -328,7 +328,7 @@ def _save(im, fp, filename): mct = info.get("mct", 0) signed = info.get("signed", False) fd = -1 - comment = info.get("comment", None) + comment = info.get("comment") add_plt = info.get("add_plt", False) if hasattr(fp, "fileno"): From 18ad4c867ff8a53a6c40d6fdf954799b976ab7f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 21:49:12 +1100 Subject: [PATCH 026/512] Use skip_unless_feature_version --- Tests/test_file_jpeg2k.py | 50 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index ccc18772a..5669af7ab 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -12,6 +12,7 @@ from .helper import ( assert_image_similar, assert_image_similar_tofile, skip_unless_feature, + skip_unless_feature_version, ) EXTRA_DIR = "Tests/images/jpeg2000" @@ -384,34 +385,31 @@ def test_custom_comment(): assert bytes(unique_comment, "utf-8") in data +@skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) - opj_version = re.search(r"(\d+\.\d+)\.\d+$", features.version_codec("jpg_2000")) - assert opj_version is not None + out = BytesIO() + test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + out.seek(0) + while True: + box_bytes = out.read(2) + if len(box_bytes) == 0: + # End of steam encountered and no PLT or SOD + break + jp2_boxid = struct.unpack(">H", box_bytes)[0] - if float(opj_version[1]) >= 2.4: - out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) - out.seek(0) - while True: - box_bytes = out.read(2) - if len(box_bytes) == 0: - # End of steam encountered and no PLT or SOD - break - jp2_boxid = struct.unpack(">H", box_bytes)[0] + if jp2_boxid == 0xFF4F: + # No length specifier for main header + continue + elif jp2_boxid == 0xFF58: + # This is the PLT box we're looking for + return + elif jp2_boxid == 0xFF93: + break + # SOD box encountered and no PLT, so it wasn't found - if jp2_boxid == 0xFF4F: - # No length specifier for main header - continue - elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for - return - elif jp2_boxid == 0xFF93: - break - # SOD box encountered and no PLT, so it wasn't found + jp2_boxlength = struct.unpack(">H", out.read(2))[0] + out.seek(jp2_boxlength - 2, os.SEEK_CUR) - jp2_boxlength = struct.unpack(">H", out.read(2))[0] - out.seek(jp2_boxlength - 2, os.SEEK_CUR) - - # The PLT box wasn't found - raise ValueError + # The PLT box wasn't found + raise ValueError From ca97e2a3a51fcad8eab4d5466b944e512b82dadd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:31:52 +1100 Subject: [PATCH 027/512] Use _binary --- Tests/test_file_jpeg2k.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 5669af7ab..56d8a7974 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -1,11 +1,17 @@ import os import re -import struct from io import BytesIO import pytest -from PIL import Image, ImageFile, Jpeg2KImagePlugin, UnidentifiedImageError, features +from PIL import ( + Image, + ImageFile, + Jpeg2KImagePlugin, + UnidentifiedImageError, + _binary, + features, +) from .helper import ( assert_image_equal, @@ -393,11 +399,11 @@ def test_plt_marker(): out.seek(0) while True: box_bytes = out.read(2) - if len(box_bytes) == 0: + if not box_bytes: # End of steam encountered and no PLT or SOD break - jp2_boxid = struct.unpack(">H", box_bytes)[0] + jp2_boxid = _binary.i16be(box_bytes) if jp2_boxid == 0xFF4F: # No length specifier for main header continue @@ -405,10 +411,10 @@ def test_plt_marker(): # This is the PLT box we're looking for return elif jp2_boxid == 0xFF93: - break # SOD box encountered and no PLT, so it wasn't found + break - jp2_boxlength = struct.unpack(">H", out.read(2))[0] + jp2_boxlength = _binary.i16be(out.read(2)) out.seek(jp2_boxlength - 2, os.SEEK_CUR) # The PLT box wasn't found From 4bb50b1fa7fe5ef4323b9cb0c819bfba2b608f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:47 +1100 Subject: [PATCH 028/512] Test comment too long --- Tests/test_file_jpeg2k.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 56d8a7974..a0fb75016 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -390,6 +390,10 @@ def test_custom_comment(): # Lazy method to determine if the comment is in the image generated assert bytes(unique_comment, "utf-8") in data + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(output_stream, "JPEG2000", comment=too_long_comment) + @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): From 04e8a9b3e723a867f220124c26489ace5d2187e0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 31 Jan 2023 22:24:52 +1100 Subject: [PATCH 029/512] Removed unnecessary code --- src/encode.c | 6 ++---- src/libImaging/Jpeg2K.h | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/encode.c b/src/encode.c index 33a2a37a7..e8946dbae 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char * comment = NULL; + char *comment = NULL; int add_plt = 0; if (!PyArg_ParseTuple( @@ -1326,7 +1326,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { PyExc_ValueError, "JPEG 2000 comment is too long"); Py_DECREF(encoder); - return NULL; + return NULL; } context->comment = strdup(comment); @@ -1338,8 +1338,6 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_DECREF(encoder); return NULL; } - } else { - context->comment = NULL; } if (quality_layers && PySequence_Check(quality_layers)) { diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 65728be5d..7bf8b4b0a 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -98,7 +98,7 @@ typedef struct { const char *error_msg; /* Custom comment */ - char * comment; + char *comment; /* Include PLT marker segment */ int add_plt; From b3af769c1a85e62600d6bbf38a9e66639a60d43c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Feb 2023 20:52:50 +1100 Subject: [PATCH 030/512] 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/512] 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 be9aea35a892b5e551e17057252403ea5a4e0929 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 28 Jan 2023 14:22:05 -0600 Subject: [PATCH 032/512] add eps test for bad BoundingBox, good ImageData --- Tests/test_file_eps.py | 16 +++++++++++++++- src/PIL/EpsImagePlugin.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 26ac2e5a1..5d63df4a6 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -56,7 +56,10 @@ simple_eps_file_with_comments = ( simple_eps_file_without_version = simple_eps_file[1:] simple_eps_file_without_boundingbox = simple_eps_file[:1] + simple_eps_file[2:] simple_eps_file_with_invalid_boundingbox = ( - simple_eps_file[:1] + (b"%%BoundingBox",) + simple_eps_file[2:] + simple_eps_file[:1] + (b"%%BoundingBox: a b c d",) + simple_eps_file[2:] +) +simple_eps_file_with_invalid_boundingbox_valid_imagedata = ( + simple_eps_file_with_invalid_boundingbox + (b"%ImageData: 100 100 8 3",) ) simple_eps_file_with_long_ascii_comment = ( simple_eps_file[:2] + (b"%%Comment: " + b"X" * 300,) + simple_eps_file[2:] @@ -128,6 +131,17 @@ def test_invalid_boundingbox_comment(prefix): EpsImagePlugin.EpsImageFile(data) +@pytest.mark.parametrize("prefix", (b"", simple_binary_header)) +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix): + data = io.BytesIO( + prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) + ) + with Image.open(data) as img: + assert img.mode == "RGB" + assert img.size == (100, 100) + assert img.format == "EPS" + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) def test_ascii_comment_too_long(prefix): data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 4c0ab0e12..2a4e804ce 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -314,7 +314,7 @@ class EpsImageFile(ImageFile.ImageFile): # Check for an "ImageData" descriptor # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:].split(None, 7)[:4] + x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] if int(bi) == 1: self.mode = "1" From bd0fac80c4f439d6fdb706889beb9b8c627c1ee8 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 17:23:57 -0600 Subject: [PATCH 033/512] deprecate EpsImagePlugin.PSFile --- Tests/test_file_eps.py | 6 ++++++ docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/9.4.0.rst | 11 +++++++++++ src/PIL/EpsImagePlugin.py | 6 ++++++ src/PIL/_deprecate.py | 4 +++- 5 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 5d63df4a6..e4c1000e2 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,6 +311,7 @@ def test_read_binary_preview(): pass +@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' @@ -346,6 +347,11 @@ def test_readline_psfile(tmp_path): _test_readline_file_psfile(s, ending) +def test_psfile_deprecation(): + with pytest.warns(DeprecationWarning): + EpsImagePlugin.PSFile(None) + + @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) @pytest.mark.parametrize( "line_ending", diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4d48b822a..c31b0dac9 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,6 +80,16 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` +PSFile +~~~~~~ + +.. deprecated:: 9.4.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index 0af5bc8ca..b7a63dd61 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,6 +1,17 @@ 9.4.0 ----- +Deprecations +============ + +PSFile +^^^^^^ + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + API Additions ============= diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2a4e804ce..6c63ef08a 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -29,6 +29,7 @@ import tempfile from . import Image, ImageFile from ._binary import i32le as i32 +from ._deprecate import deprecate # # -------------------------------------------------------------------- @@ -166,6 +167,11 @@ class PSFile: """ def __init__(self, fp): + deprecate( + "PSFile", + 11, + action="If you need the functionality of this class you will need to implement it yourself.", + ) self.fp = fp self.char = None diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 7c4b1623d..81f2189dc 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -47,8 +47,10 @@ 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__}?" + msg = f"Unknown removal version: {when}. Update {__name__}?" raise ValueError(msg) if replacement and action: From 62ab8bf80c79e1c7d8c16cf32174421f64cc8c0c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 6 Feb 2023 18:00:31 -0600 Subject: [PATCH 034/512] update "unknown version" deprecation test --- Tests/test_deprecate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_deprecate.py b/Tests/test_deprecate.py index 3375eb6b2..c7a7a9ff5 100644 --- a/Tests/test_deprecate.py +++ b/Tests/test_deprecate.py @@ -29,7 +29,7 @@ def test_version(version, expected): def test_unknown_version(): - expected = r"Unknown removal version, update PIL\._deprecate\?" + expected = r"Unknown removal version: 12345. Update PIL\._deprecate\?" with pytest.raises(ValueError, match=expected): _deprecate.deprecate("Old thing", 12345, "new thing") From 99b153c9cadbcb1c39c6747c3d97057aef054517 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:49:00 -0600 Subject: [PATCH 035/512] hyphenate "backwards-compatibility" Co-authored-by: Hugo van Kemenade --- src/PIL/EpsImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 6c63ef08a..48d32498f 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -163,7 +163,7 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): class PSFile: """ Wrapper for bytesio object that treats either CR or LF as end of line. - This class is no longer used internally, but kept for backwards-compatibility. + This class is no longer used internally, but kept for backwards compatibility. """ def __init__(self, fp): From 0f27ddafb710e104d84605a26a0e58f2a6495019 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:56:38 -0600 Subject: [PATCH 036/512] split long line --- src/PIL/EpsImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 48d32498f..9da6e946b 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -170,7 +170,8 @@ class PSFile: deprecate( "PSFile", 11, - action="If you need the functionality of this class you will need to implement it yourself.", + action="If you need the functionality of this class " + "you will need to implement it yourself.", ) self.fp = fp self.char = None From dd985b2a5e265118c36683b20afa4153a75cf69f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Tue, 7 Feb 2023 13:58:05 -0600 Subject: [PATCH 037/512] make deprecation check more specific --- Tests/test_file_eps.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index e4c1000e2..26adfff87 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -311,7 +311,6 @@ def test_read_binary_preview(): pass -@pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_readline_psfile(tmp_path): # check all the freaking line endings possible from the spec # test_string = u'something\r\nelse\n\rbaz\rbif\n' @@ -329,7 +328,8 @@ def test_readline_psfile(tmp_path): def _test_readline_io_psfile(test_string, ending): f = io.BytesIO(test_string.encode("latin-1")) - t = EpsImagePlugin.PSFile(f) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) def _test_readline_file_psfile(test_string, ending): @@ -338,7 +338,8 @@ def test_readline_psfile(tmp_path): w.write(test_string.encode("latin-1")) with open(f, "rb") as r: - t = EpsImagePlugin.PSFile(r) + with pytest.warns(DeprecationWarning): + t = EpsImagePlugin.PSFile(r) _test_readline(t, ending) for ending in line_endings: From 5059e5c143ee4b9e625163897a5610611a325bef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 08:11:50 +1100 Subject: [PATCH 038/512] 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 407489a0dc68c5e0f6143b3c767fe6f93245d81e Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 12 Feb 2023 23:24:11 +0000 Subject: [PATCH 039/512] windows: use CMake instead of MSBuild to compile liblzma --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- winbuild/build_prepare.py | 22 +++++++--------------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index d8538fbf3..67aac597b 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.12 or newer (available as Visual Studio component). +* Requires CMake 3.13 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). diff --git a/winbuild/build.rst b/winbuild/build.rst index 716669771..45a42a8ae 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,7 +21,7 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.12 or newer `_ +* `CMake 3.13 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) * x86/x64: `NASM `_ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index bef8afa9d..df207fd7e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -156,25 +156,15 @@ deps = { "filename": "xz-5.4.1.tar.gz", "dir": "xz-5.4.1", "license": "COPYING", - "patch": { - r"src\liblzma\api\lzma.h": { - "#ifndef LZMA_API_IMPORT": "#ifndef LZMA_API_IMPORT\n#define LZMA_API_STATIC", # noqa: E501 - }, - r"windows\vs2019\liblzma.vcxproj": { - # retarget to default toolset (selected by vcvarsall.bat) - "v142": "$(DefaultPlatformToolset)", # noqa: E501 - # retarget to latest (selected by vcvarsall.bat) - "10.0": "$(WindowsSDKVersion)", # noqa: E501 - }, - }, "build": [ - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Clean"), - cmd_msbuild(r"windows\vs2019\liblzma.vcxproj", "Release", "Build"), + cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_nmake(target="clean"), + cmd_nmake(target="liblzma"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], "headers": [r"src\liblzma\api\lzma.h"], - "libs": [r"windows\vs2019\Release\{msbuild_arch}\liblzma\liblzma.lib"], + "libs": [r"liblzma.lib"], }, "libwebp": { "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", @@ -215,7 +205,9 @@ deps = { }, }, "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), + cmd_cmake( + "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC" + ), cmd_nmake(target="clean"), cmd_nmake(target="tiff"), ], From abd2a3f7ee5a08ebbff3c21cd4658669d37a2fa8 Mon Sep 17 00:00:00 2001 From: nulano Date: Sun, 12 Apr 2020 13:50:31 +0200 Subject: [PATCH 040/512] windows: compile dependencies with ninja instead of nmake --- winbuild/build.rst | 6 ++- winbuild/build_prepare.py | 100 +++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 57 deletions(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index 45a42a8ae..a8d53680b 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -24,7 +24,10 @@ Download and install: * `CMake 3.13 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) -* x86/x64: `NASM `_ +* `Ninja `_ + (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) + +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -53,6 +56,7 @@ behaviour of ``build_prepare.py``: ``build_prepare.py`` also supports the following command line parameters: * ``-v`` will print generated scripts. +* ``--nmake`` will use NMake instead of Ninja for CMake dependencies * ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency * ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi (required for Raqm text shaping). diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index df207fd7e..94ecd09bf 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -55,24 +55,28 @@ def cmd_nmake(makefile=None, target="", params=None): ) -def cmd_cmake(params=None, file="."): - if params is None: - params = "" - elif isinstance(params, (list, tuple)): - params = " ".join(params) +def cmds_cmake(target, *params): + if isinstance(target, str): + targets = ("clean", target) else: - params = str(params) - return " ".join( - [ - "{cmake}", - "-DCMAKE_VERBOSE_MAKEFILE=ON", - "-DCMAKE_RULE_MESSAGES:BOOL=OFF", - "-DCMAKE_BUILD_TYPE=Release", - f"{params}", - '-G "NMake Makefiles"', - f'"{file}"', - ] - ) + targets = ("clean", *target) + + return [ + " ".join( + [ + "{cmake}", + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_VERBOSE_MAKEFILE=ON", + "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake + "-DCMAKE_C_COMPILER=cl.exe", # for Ninja + "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + *params, + '-G "{cmake_generator}"', + ".", + ] + ), + *(f"{{cmake}} --build . --target {tgt}" for tgt in targets), + ] def cmd_msbuild( @@ -118,19 +122,14 @@ deps = { ".+(libjpeg-turbo Licenses\n======================\n\n.+)$" ), "build": [ - cmd_cmake( - [ - "-DENABLE_SHARED:BOOL=FALSE", - "-DWITH_JPEG8:BOOL=TRUE", - "-DWITH_CRT_DLL:BOOL=TRUE", - ] + *cmds_cmake( + ("jpeg-static", "cjpeg-static", "djpeg-static"), + "-DENABLE_SHARED:BOOL=FALSE", + "-DWITH_JPEG8:BOOL=TRUE", + "-DWITH_CRT_DLL:BOOL=TRUE", ), - cmd_nmake(target="clean"), - cmd_nmake(target="jpeg-static"), cmd_copy("jpeg-static.lib", "libjpeg.lib"), - cmd_nmake(target="cjpeg-static"), cmd_copy("cjpeg-static.exe", "cjpeg.exe"), - cmd_nmake(target="djpeg-static"), cmd_copy("djpeg-static.exe", "djpeg.exe"), ], "headers": ["j*.h"], @@ -157,9 +156,7 @@ deps = { "dir": "xz-5.4.1", "license": "COPYING", "build": [ - cmd_cmake("-DBUILD_SHARED_LIBS:BOOL=OFF"), - cmd_nmake(target="clean"), - cmd_nmake(target="liblzma"), + *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), cmd_mkdir(r"{inc_dir}\lzma"), cmd_copy(r"src\liblzma\api\lzma\*.h", r"{inc_dir}\lzma"), ], @@ -205,11 +202,11 @@ deps = { }, }, "build": [ - cmd_cmake( - "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC" - ), - cmd_nmake(target="clean"), - cmd_nmake(target="tiff"), + *cmds_cmake( + "tiff", + "-DBUILD_SHARED_LIBS:BOOL=OFF", + "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC", + ) ], "headers": [r"libtiff\tiff*.h"], "libs": [r"libtiff\*.lib"], @@ -221,10 +218,7 @@ deps = { "dir": "lpng1639", "license": "LICENSE", "build": [ - # lint: do not inline - cmd_cmake(("-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(), + *cmds_cmake("png_static", "-DPNG_SHARED:BOOL=OFF", "-DPNG_TESTS:BOOL=OFF"), cmd_copy("libpng16_static.lib", "libpng16.lib"), ], "headers": [r"png*.h"], @@ -236,10 +230,7 @@ deps = { "dir": "brotli-1.0.9", "license": "LICENSE", "build": [ - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="brotlicommon-static"), - cmd_nmake(target="brotlidec-static"), + *cmds_cmake(("brotlicommon-static", "brotlidec-static")), cmd_xcopy(r"c\include", "{inc_dir}"), ], "libs": ["*.lib"], @@ -317,9 +308,9 @@ deps = { } }, "build": [ - cmd_cmake(("-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF")), - cmd_nmake(target="clean"), - cmd_nmake(target="openjp2"), + *cmds_cmake( + "openjp2", "-DBUILD_CODEC:BOOL=OFF", "-DBUILD_SHARED_LIBS:BOOL=OFF" + ), cmd_mkdir(r"{inc_dir}\openjpeg-2.5.0"), cmd_copy(r"src\lib\openjp2\*.h", r"{inc_dir}\openjpeg-2.5.0"), ], @@ -338,10 +329,7 @@ deps = { } }, "build": [ - # lint: do not inline - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="imagequant_a"), + *cmds_cmake("imagequant_a"), cmd_copy("imagequant_a.lib", "imagequant.lib"), ], "headers": [r"*.h"], @@ -354,9 +342,7 @@ deps = { "license": "COPYING", "build": [ cmd_set("CXXFLAGS", "-d2FH4-"), - cmd_cmake("-DHB_HAVE_FREETYPE:BOOL=TRUE"), - cmd_nmake(target="clean"), - cmd_nmake(target="harfbuzz"), + *cmds_cmake("harfbuzz", "-DHB_HAVE_FREETYPE:BOOL=TRUE"), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], @@ -369,9 +355,7 @@ deps = { "build": [ cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), - cmd_cmake(), - cmd_nmake(target="clean"), - cmd_nmake(target="fribidi"), + *cmds_cmake("fribidi"), ], "bins": [r"*.dll"], }, @@ -600,10 +584,13 @@ if __name__ == "__main__": else ("x86" if struct.calcsize("P") == 4 else "x64"), ) build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) + cmake_generator = "Ninja" sources_dir = "" for arg in sys.argv[1:]: if arg == "-v": verbose = True + elif arg == "--nmake": + cmake_generator = "NMake Makefiles" elif arg == "--no-imagequant": disabled += ["libimagequant"] elif arg == "--no-raqm" or arg == "--no-fribidi": @@ -679,6 +666,7 @@ if __name__ == "__main__": # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically + "cmake_generator": cmake_generator, # TODO find NASM automatically # script header "header": sum([header, msvs["header"], ["@echo on"]], []), From 57260d49242dded68dfd0287a2c3d17eb327a81c Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 01:34:00 +0000 Subject: [PATCH 041/512] suppress MSVC compiler logo output when using ninja --- winbuild/build_prepare.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 94ecd09bf..f643c4a08 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -56,10 +56,8 @@ def cmd_nmake(makefile=None, target="", params=None): def cmds_cmake(target, *params): - if isinstance(target, str): - targets = ("clean", target) - else: - targets = ("clean", *target) + if not isinstance(target, str): + target = " ".join(target) return [ " ".join( @@ -70,12 +68,14 @@ def cmds_cmake(target, *params): "-DCMAKE_RULE_MESSAGES:BOOL=OFF", # for NMake "-DCMAKE_C_COMPILER=cl.exe", # for Ninja "-DCMAKE_CXX_COMPILER=cl.exe", # for Ninja + "-DCMAKE_C_FLAGS=-nologo", + "-DCMAKE_CXX_FLAGS=-nologo", *params, '-G "{cmake_generator}"', ".", ] ), - *(f"{{cmake}} --build . --target {tgt}" for tgt in targets), + f"{{cmake}} --build . --clean-first --parallel --target {target}", ] @@ -205,7 +205,7 @@ deps = { *cmds_cmake( "tiff", "-DBUILD_SHARED_LIBS:BOOL=OFF", - "-DCMAKE_C_FLAGS=-DLZMA_API_STATIC", + '-DCMAKE_C_FLAGS="-nologo -DLZMA_API_STATIC"', ) ], "headers": [r"libtiff\tiff*.h"], @@ -341,8 +341,11 @@ deps = { "dir": "harfbuzz-7.0.0", "license": "COPYING", "build": [ - cmd_set("CXXFLAGS", "-d2FH4-"), - *cmds_cmake("harfbuzz", "-DHB_HAVE_FREETYPE:BOOL=TRUE"), + *cmds_cmake( + "harfbuzz", + "-DHB_HAVE_FREETYPE:BOOL=TRUE", + '-DCMAKE_CXX_FLAGS="-nologo -d2FH4-"', + ), ], "headers": [r"src\*.h"], "libs": [r"*.lib"], From eeb7c7c647cacc59189a5ee7891d38f23d19721a Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 03:16:04 +0000 Subject: [PATCH 042/512] windows: parse build configuration with argparse --- .github/workflows/test-windows.yml | 2 +- winbuild/build.rst | 60 +++++----- winbuild/build_prepare.py | 179 ++++++++++++++++++----------- 3 files changed, 149 insertions(+), 92 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 306e34ca9..30084d093 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -88,7 +88,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python=$env:pythonLocation --srcdir + & python.exe winbuild\build_prepare.py -v --python $env:pythonLocation shell: pwsh - name: Build dependencies / libjpeg-turbo diff --git a/winbuild/build.rst b/winbuild/build.rst index a8d53680b..c13acf50d 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -38,42 +38,50 @@ Visual Studio is found automatically with ``vswhere.exe``. Build configuration ------------------- -The following environment variables, if set, will override the default -behaviour of ``build_prepare.py``: +Run ``build_prepare.py`` to configure the build:: -* ``PYTHON`` + ``EXECUTABLE`` point to the target version of Python. - If ``PYTHON`` is unset, the version of Python used to run - ``build_prepare.py`` will be used. If only ``PYTHON`` is set, - ``EXECUTABLE`` defaults to ``python.exe``. -* ``ARCHITECTURE`` is used to select a ``x86``, ``x64`` or ``ARM64`` build. - By default, uses same architecture as the version of Python used to run ``build_prepare.py``. -* ``PILLOW_BUILD`` can be used to override the ``winbuild\build`` directory - path, used to store generated build scripts and compiled libraries. - **Warning:** This directory is wiped when ``build_prepare.py`` is run. -* ``PILLOW_DEPS`` points to the directory used to store downloaded - dependencies. By default ``winbuild\depends`` is used. + usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] + [--depends PILLOW_DEPS] + [--architecture {x86,x64,ARM64}] + [--python PYTHON] [--executable EXECUTABLE] + [--nmake] [--no-imagequant] [--no-fribidi] -``build_prepare.py`` also supports the following command line parameters: + Download dependencies and generate build scripts for Pillow. -* ``-v`` will print generated scripts. -* ``--nmake`` will use NMake instead of Ninja for CMake dependencies -* ``--no-imagequant`` will skip GPL-licensed ``libimagequant`` optional dependency -* ``--no-fribidi`` or ``--no-raqm`` will skip optional LGPL-licensed dependency FriBiDi - (required for Raqm text shaping). -* ``--python=`` and ``--executable=`` override ``PYTHON`` and ``EXECUTABLE``. -* ``--architecture=`` overrides ``ARCHITECTURE``. -* ``--dir=`` and ``--depends=`` override ``PILLOW_BUILD`` - and ``PILLOW_DEPS``. + options: + -h, --help show this help message and exit + -v, --verbose print generated scripts + -d PILLOW_BUILD, --dir PILLOW_BUILD, --build-dir PILLOW_BUILD + build directory (default: 'winbuild\build') + --depends PILLOW_DEPS + directory used to store cached dependencies (default: + 'winbuild\depends') + --architecture {x86,x64,ARM64} + build architecture (default: same as host python) + --python PYTHON Python install directory (default: use host python) + --executable EXECUTABLE + Python executable (default: use host python) + --nmake build dependencies using NMake instead of Ninja + --no-imagequant skip GPL-licensed optional dependency libimagequant + --no-fribidi, --no-raqm + skip LGPL-licensed optional dependency FriBiDi + + Arguments can also be supplied using the environment variables PILLOW_BUILD, + PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more + information. + +**Warning:** The build directory is wiped when ``build_prepare.py`` is run. Dependencies ------------ Dependencies will be automatically downloaded by ``build_prepare.py``. By default, downloaded dependencies are stored in ``winbuild\depends``; -set the ``PILLOW_DEPS`` environment variable to override this location. +use the ``--depends`` argument or ``PILLOW_DEPS`` environment variable +to override this location. To build all dependencies, run ``winbuild\build\build_dep_all.cmd``, -or run the individual scripts to build each dependency separately. +or run the individual scripts in order to build each dependency separately. Building Pillow --------------- @@ -106,7 +114,7 @@ The following is a simplified version of the script used on AppVeyor: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends + C:\Python37\bin\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f643c4a08..de07810a8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -1,3 +1,4 @@ +import argparse import os import platform import re @@ -434,7 +435,7 @@ def extract_dep(url, filename): import urllib.request import zipfile - file = os.path.join(depends_dir, filename) + file = os.path.join(args.depends_dir, filename) if not os.path.exists(file): ex = None for i in range(3): @@ -475,12 +476,12 @@ def extract_dep(url, filename): def write_script(name, lines): - name = os.path.join(build_dir, name) + name = os.path.join(args.build_dir, name) lines = [line.format(**prefs) for line in lines] print("Writing " + name) with open(name, "w", newline="") as f: f.write(os.linesep.join(lines)) - if verbose: + if args.verbose: for line in lines: print(" " + line) @@ -549,11 +550,14 @@ def build_dep(name): def build_dep_all(): lines = ["@echo on"] for dep_name in deps: + print() if dep_name in disabled: + print(f"Skipping disabled dependency {dep_name}") continue script = build_dep(dep_name) lines.append(rf'cmd.exe /c "{{build_dir}}\{script}"') lines.append("if errorlevel 1 echo Build failed! && exit /B 1") + print() lines.append("@echo All Pillow dependencies built successfully!") write_script("build_dep_all.cmd", lines) @@ -572,59 +576,90 @@ def build_pillow(): if __name__ == "__main__": - # winbuild directory winbuild_dir = os.path.dirname(os.path.realpath(__file__)) + pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) - verbose = False - disabled = [] - depends_dir = os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")) - python_dir = os.environ.get("PYTHON") - python_exe = os.environ.get("EXECUTABLE", "python.exe") - architecture = os.environ.get( - "ARCHITECTURE", - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64"), + parser = argparse.ArgumentParser( + prog="winbuild\\build_prepare.py", + description="Download dependencies and generate build scripts for Pillow.", + epilog="""Arguments can also be supplied using the environment variables + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. + See winbuild\\build.rst for more information.""", ) - build_dir = os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")) - cmake_generator = "Ninja" - sources_dir = "" - for arg in sys.argv[1:]: - if arg == "-v": - verbose = True - elif arg == "--nmake": - cmake_generator = "NMake Makefiles" - elif arg == "--no-imagequant": - disabled += ["libimagequant"] - elif arg == "--no-raqm" or arg == "--no-fribidi": - disabled += ["fribidi"] - elif arg.startswith("--depends="): - depends_dir = arg[10:] - elif arg.startswith("--python="): - python_dir = arg[9:] - elif arg.startswith("--executable="): - python_exe = arg[13:] - elif arg.startswith("--architecture="): - architecture = arg[15:] - elif arg.startswith("--dir="): - build_dir = arg[6:] - elif arg == "--srcdir": - sources_dir = os.path.sep + "src" - else: - msg = "Unknown parameter: " + arg - raise ValueError(msg) + parser.add_argument( + "-v", "--verbose", action="store_true", help="print generated scripts" + ) + parser.add_argument( + "-d", + "--dir", + "--build-dir", + dest="build_dir", + metavar="PILLOW_BUILD", + default=os.environ.get("PILLOW_BUILD", os.path.join(winbuild_dir, "build")), + help="build directory (default: 'winbuild\\build')", + ) + parser.add_argument( + "--depends", + dest="depends_dir", + metavar="PILLOW_DEPS", + default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), + help="directory used to store cached dependencies (default: 'winbuild\\depends')", # noqa: E501 + ) + parser.add_argument( + "--architecture", + choices=architectures, + default=os.environ.get( + "ARCHITECTURE", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), + ), + help="build architecture (default: same as host python)", + ) + parser.add_argument( + "--python", + dest="python_dir", + metavar="PYTHON", + default=os.environ.get("PYTHON"), + help="Python install directory (default: use host python)", + ) + parser.add_argument( + "--executable", + dest="python_exe", + metavar="EXECUTABLE", + default=os.environ.get("EXECUTABLE", "python.exe"), + help="Python executable (default: use host python)", + ) + parser.add_argument( + "--nmake", + dest="cmake_generator", + action="store_const", + const="NMake Makefiles", + default="Ninja", + help="build dependencies using NMake instead of Ninja", + ) + parser.add_argument( + "--no-imagequant", + action="store_true", + help="skip GPL-licensed optional dependency libimagequant", + ) + parser.add_argument( + "--no-fribidi", + "--no-raqm", + action="store_true", + help="skip LGPL-licensed optional dependency FriBiDi", + ) + args = parser.parse_args() - # dependency cache directory - os.makedirs(depends_dir, exist_ok=True) - print("Caching dependencies in:", depends_dir) + arch_prefs = architectures[args.architecture] + print("Target Architecture:", args.architecture) - if python_dir is None: - python_dir = os.path.dirname(os.path.realpath(sys.executable)) - python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(python_dir, python_exe)) - - arch_prefs = architectures[architecture] - print("Target Architecture:", architecture) + if args.python_dir is None: + args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) + args.python_exe = os.path.basename(sys.executable) + print("Target Python:", os.path.join(args.python_dir, args.python_exe)) msvs = find_msvs() if msvs is None: @@ -632,35 +667,47 @@ if __name__ == "__main__": raise RuntimeError(msg) print("Found Visual Studio at:", msvs["vs_dir"]) - print("Using output directory:", build_dir) + # dependency cache directory + args.depends_dir = os.path.abspath(args.depends_dir) + os.makedirs(args.depends_dir, exist_ok=True) + print("Caching dependencies in:", args.depends_dir) + + args.build_dir = os.path.abspath(args.build_dir) + print("Using output directory:", args.build_dir) # build directory for *.h files - inc_dir = os.path.join(build_dir, "inc") + inc_dir = os.path.join(args.build_dir, "inc") # build directory for *.lib files - lib_dir = os.path.join(build_dir, "lib") + lib_dir = os.path.join(args.build_dir, "lib") # build directory for *.bin files - bin_dir = os.path.join(build_dir, "bin") + bin_dir = os.path.join(args.build_dir, "bin") # directory for storing project files - sources_dir = build_dir + sources_dir + sources_dir = os.path.join(args.build_dir, "src") # copy dependency licenses to this directory - license_dir = os.path.join(build_dir, "license") + license_dir = os.path.join(args.build_dir, "license") - shutil.rmtree(build_dir, ignore_errors=True) - os.makedirs(build_dir, exist_ok=False) + shutil.rmtree(args.build_dir, ignore_errors=True) + os.makedirs(args.build_dir, exist_ok=False) for path in [inc_dir, lib_dir, bin_dir, sources_dir, license_dir]: os.makedirs(path, exist_ok=True) + disabled = [] + if args.no_imagequant: + disabled += ["libimagequant"] + if args.no_fribidi: + disabled += ["fribidi"] + prefs = { # Python paths / preferences - "python_dir": python_dir, - "python_exe": python_exe, - "architecture": architecture, + "python_dir": args.python_dir, + "python_exe": args.python_exe, + "architecture": args.architecture, **arch_prefs, # Pillow paths - "pillow_dir": os.path.realpath(os.path.join(winbuild_dir, "..")), + "pillow_dir": pillow_dir, "winbuild_dir": winbuild_dir, # Build paths - "build_dir": build_dir, + "build_dir": args.build_dir, "inc_dir": inc_dir, "lib_dir": lib_dir, "bin_dir": bin_dir, @@ -669,7 +716,7 @@ if __name__ == "__main__": # Compilers / Tools **msvs, "cmake": "cmake.exe", # TODO find CMAKE automatically - "cmake_generator": cmake_generator, + "cmake_generator": args.cmake_generator, # TODO find NASM automatically # script header "header": sum([header, msvs["header"], ["@echo on"]], []), @@ -682,4 +729,6 @@ if __name__ == "__main__": write_script(".gitignore", ["*"]) build_dep_all() + if args.verbose: + print() build_pillow() From c5e1b5ad6694aa3b67277ee09a41d3408f645ddf Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 13 Feb 2023 15:26:00 +0000 Subject: [PATCH 043/512] use consistent capitalization Co-authored-by: Hugo van Kemenade --- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/winbuild/build.rst b/winbuild/build.rst index c13acf50d..6beb5d655 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -57,10 +57,10 @@ Run ``build_prepare.py`` to configure the build:: directory used to store cached dependencies (default: 'winbuild\depends') --architecture {x86,x64,ARM64} - build architecture (default: same as host python) - --python PYTHON Python install directory (default: use host python) + build architecture (default: same as host Python) + --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE - Python executable (default: use host python) + Python executable (default: use host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index de07810a8..359aad817 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -603,7 +603,8 @@ if __name__ == "__main__": dest="depends_dir", metavar="PILLOW_DEPS", default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), - help="directory used to store cached dependencies (default: 'winbuild\\depends')", # noqa: E501 + help="directory used to store cached dependencies " + "(default: 'winbuild\\depends')", ) parser.add_argument( "--architecture", @@ -616,21 +617,21 @@ if __name__ == "__main__": else ("x86" if struct.calcsize("P") == 4 else "x64") ), ), - help="build architecture (default: same as host python)", + help="build architecture (default: same as host Python)", ) parser.add_argument( "--python", dest="python_dir", metavar="PYTHON", default=os.environ.get("PYTHON"), - help="Python install directory (default: use host python)", + help="Python install directory (default: use host Python)", ) parser.add_argument( "--executable", dest="python_exe", metavar="EXECUTABLE", default=os.environ.get("EXECUTABLE", "python.exe"), - help="Python executable (default: use host python)", + help="Python executable (default: use host Python)", ) parser.add_argument( "--nmake", @@ -654,7 +655,7 @@ if __name__ == "__main__": args = parser.parse_args() arch_prefs = architectures[args.architecture] - print("Target Architecture:", args.architecture) + print("Target architecture:", args.architecture) if args.python_dir is None: args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) From 91a53ed28075366ca7fa6bdb3591a66f8e03c7d4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 15:26:50 +0000 Subject: [PATCH 044/512] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 359aad817..221a77704 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -604,7 +604,7 @@ if __name__ == "__main__": metavar="PILLOW_DEPS", default=os.environ.get("PILLOW_DEPS", os.path.join(winbuild_dir, "depends")), help="directory used to store cached dependencies " - "(default: 'winbuild\\depends')", + "(default: 'winbuild\\depends')", ) parser.add_argument( "--architecture", From b05bc346045f4d243cdaecf85ff61567e5ca377c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Dec 2022 15:42:38 +1100 Subject: [PATCH 045/512] Test lists and tuples --- Tests/check_jpeg_leaks.py | 12 +-- Tests/test_imagedraw.py | 177 ++++++++++++++++++++++---------------- Tests/test_imagedraw2.py | 30 ++++--- 3 files changed, 128 insertions(+), 91 deletions(-) diff --git a/Tests/check_jpeg_leaks.py b/Tests/check_jpeg_leaks.py index ab8d77719..5d95ca29c 100644 --- a/Tests/check_jpeg_leaks.py +++ b/Tests/check_jpeg_leaks.py @@ -110,11 +110,13 @@ def test_qtables_leak(): ) ] - qtables = [standard_l_qtable, standard_chrominance_qtable] - - for _ in range(iterations): - test_output = BytesIO() - im.save(test_output, "JPEG", qtables=qtables) + for qtables in ( + (standard_l_qtable, standard_chrominance_qtable), + [standard_l_qtable, standard_chrominance_qtable], + ): + for _ in range(iterations): + test_output = BytesIO() + im.save(test_output, "JPEG", qtables=qtables) def test_exif_leak(): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index d4723c924..f6f27d32d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -27,15 +27,21 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +KITE_POINTS = ( + ((10, 50), (70, 10), (90, 50), (70, 90), (10, 50)), + [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)], +) def test_sanity(): @@ -63,7 +69,7 @@ def test_mode_mismatch(): ImageDraw.ImageDraw(im, mode="L") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) def test_arc(bbox, start, end): # Arrange @@ -77,7 +83,8 @@ def test_arc(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_arc.png", 1) -def test_arc_end_le_start(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_end_le_start(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -85,13 +92,14 @@ def test_arc_end_le_start(): end = 0 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_arc_end_le_start.png") -def test_arc_no_loops(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_no_loops(bbox): # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -100,57 +108,61 @@ def test_arc_no_loops(): end = 370 # Act - draw.arc(BBOX1, start=start, end=end) + draw.arc(bbox, start=start, end=end) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_no_loops.png", 1) -def test_arc_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, width=5) + draw.arc(bbox, 10, 260, width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width.png", 1) -def test_arc_width_pieslice_large(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_pieslice_large(bbox): # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=100) + draw.arc(bbox, 10, 260, fill="yellow", width=100) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_pieslice.png", 1) -def test_arc_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.arc(BBOX1, 10, 260, fill="yellow", width=5) + draw.arc(bbox, 10, 260, fill="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_arc_width_fill.png", 1) -def test_arc_width_non_whole_angle(): +@pytest.mark.parametrize("bbox", BBOX) +def test_arc_width_non_whole_angle(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_arc_width_non_whole_angle.png" # Act - draw.arc(BBOX1, 10, 259.5, width=5) + draw.arc(bbox, 10, 259.5, width=5) # Assert assert_image_similar_tofile(im, expected, 1) @@ -184,7 +196,7 @@ def test_bitmap(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_chord(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -198,37 +210,40 @@ def test_chord(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_chord_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, outline="yellow", width=5) + draw.chord(bbox, 10, 260, outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width.png", 1) -def test_chord_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=5) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_chord_width_fill.png", 1) -def test_chord_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_chord_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.chord(BBOX1, 10, 260, fill="red", outline="yellow", width=0) + draw.chord(bbox, 10, 260, fill="red", outline="yellow", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_chord_zero_width.png") @@ -247,7 +262,7 @@ def test_chord_too_fat(): @pytest.mark.parametrize("mode", ("RGB", "L")) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(mode, bbox): # Arrange im = Image.new(mode, (W, H)) @@ -261,13 +276,14 @@ def test_ellipse(mode, bbox): assert_image_similar_tofile(im, expected, 1) -def test_ellipse_translucent(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_translucent(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.ellipse(BBOX1, fill=(0, 255, 0, 127)) + draw.ellipse(bbox, fill=(0, 255, 0, 127)) # Assert expected = "Tests/images/imagedraw_ellipse_translucent.png" @@ -297,13 +313,14 @@ def test_ellipse_symmetric(): assert_image_equal(im, im.transpose(Image.Transpose.FLIP_LEFT_RIGHT)) -def test_ellipse_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, outline="blue", width=5) + draw.ellipse(bbox, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width.png", 1) @@ -321,25 +338,27 @@ def test_ellipse_width_large(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_large.png", 1) -def test_ellipse_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=5) + draw.ellipse(bbox, fill="green", outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_width_fill.png", 1) -def test_ellipse_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_ellipse_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.ellipse(BBOX1, fill="green", outline="blue", width=0) + draw.ellipse(bbox, fill="green", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") @@ -380,7 +399,7 @@ def test_ellipse_various_sizes_filled(): ) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -452,7 +471,7 @@ def test_transform(): assert_image_equal(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice(bbox, start, end): # Arrange @@ -466,38 +485,41 @@ def test_pieslice(bbox, start, end): assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice.png", 1) -def test_pieslice_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, outline="blue", width=5) + draw.pieslice(bbox, 10, 260, outline="blue", width=5) # Assert assert_image_similar_tofile(im, "Tests/images/imagedraw_pieslice_width.png", 1) -def test_pieslice_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_pieslice_width_fill.png" # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=5) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=5) # Assert assert_image_similar_tofile(im, expected, 1) -def test_pieslice_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_pieslice_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.pieslice(BBOX1, 10, 260, fill="white", outline="blue", width=0) + draw.pieslice(bbox, 10, 260, fill="white", outline="blue", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_pieslice_zero_width.png") @@ -545,7 +567,7 @@ def test_pieslice_no_spikes(): assert_image_equal(im, im_pre_erase) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_point(points): # Arrange im = Image.new("RGB", (W, H)) @@ -558,7 +580,7 @@ def test_point(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_point.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -572,7 +594,8 @@ def test_polygon(points): @pytest.mark.parametrize("mode", ("RGB", "L")) -def test_polygon_kite(mode): +@pytest.mark.parametrize("kite_points", KITE_POINTS) +def test_polygon_kite(mode, kite_points): # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -581,7 +604,7 @@ def test_polygon_kite(mode): expected = f"Tests/images/imagedraw_polygon_kite_{mode}.png" # Act - draw.polygon(KITE_POINTS, fill="blue", outline="yellow") + draw.polygon(kite_points, fill="blue", outline="yellow") # Assert assert_image_equal_tofile(im, expected) @@ -628,7 +651,7 @@ def test_polygon_translucent(): assert_image_equal_tofile(im, expected) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -655,63 +678,68 @@ def test_big_rectangle(): assert_image_similar_tofile(im, "Tests/images/imagedraw_big_rectangle.png", 1) -def test_rectangle_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width.png" # Act - draw.rectangle(BBOX1, outline="green", width=5) + draw.rectangle(bbox, outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_width_fill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_width_fill(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) expected = "Tests/images/imagedraw_rectangle_width_fill.png" # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=5) + draw.rectangle(bbox, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, expected) -def test_rectangle_zero_width(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_zero_width(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="blue", outline="green", width=0) + draw.rectangle(bbox, fill="blue", outline="green", width=0) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_zero_width.png") -def test_rectangle_I16(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_I16(bbox): # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rectangle(BBOX1, fill="black", outline="green") + draw.rectangle(bbox, fill="black", outline="green") # Assert assert_image_equal_tofile(im.convert("I"), "Tests/images/imagedraw_rectangle_I.png") -def test_rectangle_translucent_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rectangle_translucent_outline(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") # Act - draw.rectangle(BBOX1, fill="black", outline=(0, 255, 0, 127), width=5) + draw.rectangle(bbox, fill="black", outline=(0, 255, 0, 127), width=5) # Assert assert_image_equal_tofile( @@ -758,13 +786,14 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type): ) -def test_rounded_rectangle_zero_radius(): +@pytest.mark.parametrize("bbox", BBOX) +def test_rounded_rectangle_zero_radius(bbox): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) # Act - draw.rounded_rectangle(BBOX1, 0, fill="blue", outline="green", width=5) + draw.rounded_rectangle(bbox, 0, fill="blue", outline="green", width=5) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_rectangle_width_fill.png") @@ -794,14 +823,15 @@ def test_rounded_rectangle_translucent(xy, suffix): ) -def test_floodfill(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill(bbox): red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -826,13 +856,14 @@ def test_floodfill(): assert_image_equal(im, Image.new("RGB", (1, 1), red)) -def test_floodfill_border(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_border(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="yellow", fill="green") + draw.rectangle(bbox, outline="yellow", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -847,13 +878,14 @@ def test_floodfill_border(): assert_image_equal_tofile(im, "Tests/images/imagedraw_floodfill2.png") -def test_floodfill_thresh(): +@pytest.mark.parametrize("bbox", BBOX) +def test_floodfill_thresh(bbox): # floodfill() is experimental # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) - draw.rectangle(BBOX2, outline="darkgreen", fill="green") + draw.rectangle(bbox, outline="darkgreen", fill="green") centre_point = (int(W / 2), int(H / 2)) # Act @@ -1291,7 +1323,8 @@ def test_setting_default_font(): assert isinstance(draw.getfont(), ImageFont.ImageFont) -def test_same_color_outline(): +@pytest.mark.parametrize("bbox", BBOX) +def test_same_color_outline(bbox): # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1307,12 +1340,12 @@ def test_same_color_outline(): for mode in ["RGB", "L"]: for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: for operation, args in { - "chord": [BBOX1, 0, 180], - "ellipse": [BBOX1], + "chord": [bbox, 0, 180], + "ellipse": [bbox], "shape": [s], - "pieslice": [BBOX1, -90, 45], + "pieslice": [bbox, -90, 45], "polygon": [[(18, 30), (85, 30), (60, 72)]], - "rectangle": [BBOX1], + "rectangle": [bbox], }.items(): # Arrange im = Image.new(mode, (W, H)) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 6fc829f1a..143341b0a 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -27,15 +27,16 @@ X1 = int(X0 * 3) Y0 = int(H / 4) Y1 = int(X0 * 3) -# Two kinds of bounding box -BBOX1 = [(X0, Y0), (X1, Y1)] -BBOX2 = [X0, Y0, X1, Y1] +# Bounding boxes +BBOX = (((X0, Y0), (X1, Y1)), [(X0, Y0), (X1, Y1)], (X0, Y0, X1, Y1), [X0, Y0, X1, Y1]) -# Two kinds of coordinate sequences -POINTS1 = [(10, 10), (20, 40), (30, 30)] -POINTS2 = [10, 10, 20, 40, 30, 30] - -KITE_POINTS = [(10, 50), (70, 10), (90, 50), (70, 90), (10, 50)] +# Coordinate sequences +POINTS = ( + ((10, 10), (20, 40), (30, 30)), + [(10, 10), (20, 40), (30, 30)], + (10, 10, 20, 40, 30, 30), + [10, 10, 20, 40, 30, 30], +) FONT_PATH = "Tests/fonts/FreeMono.ttf" @@ -52,7 +53,7 @@ def test_sanity(): draw.line(list(range(10)), pen) -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_ellipse(bbox): # Arrange im = Image.new("RGB", (W, H)) @@ -80,7 +81,7 @@ def test_ellipse_edge(): assert_image_similar_tofile(im, "Tests/images/imagedraw_ellipse_edge.png", 1) -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_line(points): # Arrange im = Image.new("RGB", (W, H)) @@ -94,7 +95,8 @@ def test_line(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -def test_line_pen_as_brush(): +@pytest.mark.parametrize("points", POINTS) +def test_line_pen_as_brush(points): # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw2.Draw(im) @@ -103,13 +105,13 @@ def test_line_pen_as_brush(): # Act # Pass in the pen as the brush parameter - draw.line(POINTS1, pen, brush) + draw.line(points, pen, brush) # Assert assert_image_equal_tofile(im, "Tests/images/imagedraw_line.png") -@pytest.mark.parametrize("points", (POINTS1, POINTS2)) +@pytest.mark.parametrize("points", POINTS) def test_polygon(points): # Arrange im = Image.new("RGB", (W, H)) @@ -124,7 +126,7 @@ def test_polygon(points): assert_image_equal_tofile(im, "Tests/images/imagedraw_polygon.png") -@pytest.mark.parametrize("bbox", (BBOX1, BBOX2)) +@pytest.mark.parametrize("bbox", BBOX) def test_rectangle(bbox): # Arrange im = Image.new("RGB", (W, H)) From 0f2a4c1ae5d8492280af126dc7fda9eeef9b9d50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Feb 2023 19:19:17 +1100 Subject: [PATCH 046/512] 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 047/512] 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 048/512] 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 5c8a9165abcf7c2d2ec2829681e88726dafddaf4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 23 Feb 2023 15:18:11 +0200 Subject: [PATCH 049/512] 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 050/512] 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 742aff3718cebdad390ad808e3cae181ea749e27 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 08:17:10 +1100 Subject: [PATCH 051/512] 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 44c4e67fe13719672219aa2de67b8c19f921c191 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:26:18 +0200 Subject: [PATCH 052/512] 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 053/512] 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 054/512] 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 055/512] 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 056/512] 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 057/512] 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 058/512] 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 13:40:44 +1100 Subject: [PATCH 059/512] 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 060/512] 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 061/512] 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 062/512] 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 063/512] 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 064/512] 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 065/512] 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 066/512] 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 067/512] 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 068/512] 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 4f9c3847e85a210e5b57269b04ef7ad6179ad698 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 14:21:17 -0600 Subject: [PATCH 069/512] notes about %ImageData, and use better var names --- src/PIL/EpsImagePlugin.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 9da6e946b..52036b0ac 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -320,20 +320,32 @@ class EpsImageFile(ImageFile.ImageFile): elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor - # Encoded bitmapped image. - x, y, bi, mo = byte_arr[11:bytes_read].split(None, 7)[:4] + # Values: + # columns + # rows + # bit depth + # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) + # number of padding channels + # block size (number of bytes per row per channel) + # binary/ascii (1: binary, 2: ascii) + # data start identifier (the image data follows after a single line + # consisting only of this quoted value) + image_data_values = byte_arr[11:bytes_read].split(None, 7) + columns, rows, bit_depth, mode_id = [ + int(value) for value in image_data_values[:4] + ] - if int(bi) == 1: + if bit_depth == 1: self.mode = "1" - elif int(bi) == 8: + elif bit_depth == 8: try: - self.mode = self.mode_map[int(mo)] + self.mode = self.mode_map[mode_id] except ValueError: break else: break - self._size = int(x), int(y) + self._size = columns, rows return bytes_read = 0 From 60b717a94b98d4d56fb21b758330e613e5d3ae24 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 2 Mar 2023 15:26:06 -0600 Subject: [PATCH 070/512] add link to %ImageData definition and remove empty comment lines --- src/PIL/EpsImagePlugin.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 52036b0ac..1c88d22c7 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -31,9 +31,9 @@ from . import Image, ImageFile from ._binary import i32le as i32 from ._deprecate import deprecate -# # -------------------------------------------------------------------- + split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$") field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$") @@ -319,11 +319,12 @@ class EpsImageFile(ImageFile.ImageFile): raise OSError(msg) elif bytes_mv[:11] == b"%ImageData:": # Check for an "ImageData" descriptor + # https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577413_pgfId-1035096 # Values: # columns # rows - # bit depth + # bit depth (1 or 8) # mode (1: L, 2: LAB, 3: RGB, 4: CMYK) # number of padding channels # block size (number of bytes per row per channel) @@ -395,18 +396,15 @@ class EpsImageFile(ImageFile.ImageFile): pass -# # -------------------------------------------------------------------- def _save(im, fp, filename, eps=1): """EPS Writer for the Python Imaging Library.""" - # # make sure image data is available im.load() - # # determine PostScript image mode if im.mode == "L": operator = (8, 1, b"image") @@ -419,7 +417,6 @@ def _save(im, fp, filename, eps=1): raise ValueError(msg) if eps: - # # write EPS header fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n") fp.write(b"%%Creator: PIL 0.1 EpsEncode\n") @@ -431,7 +428,6 @@ def _save(im, fp, filename, eps=1): fp.write(b"%%ImageData: %d %d " % im.size) fp.write(b'%d %d 0 1 1 "%s"\n' % operator) - # # image header fp.write(b"gsave\n") fp.write(b"10 dict begin\n") @@ -452,7 +448,6 @@ def _save(im, fp, filename, eps=1): fp.flush() -# # -------------------------------------------------------------------- From 1a790a91f57ac764d77fb8e5d23b82669419e391 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 3 Mar 2023 14:38:51 +1100 Subject: [PATCH 071/512] 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 072/512] 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 073/512] 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 074/512] 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 075/512] 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 076/512] 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 077/512] 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 078/512] 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 079/512] 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 080/512] 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 081/512] 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 082/512] 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 083/512] 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 084/512] 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 085/512] 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 086/512] 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 1690592d8b514e4580384a6ca297540654a5f04b Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 6 Mar 2023 02:24:00 +0000 Subject: [PATCH 087/512] correct minimum CMake version --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 67aac597b..21b40d4e6 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -10,7 +10,7 @@ For more extensive info, see the [Windows build instructions](build.rst). * Requires Microsoft Visual Studio 2017 or newer with C++ component. * Requires NASM for libjpeg-turbo, a required dependency when using this script. -* Requires CMake 3.13 or newer (available as Visual Studio component). +* Requires CMake 3.15 or newer (available as Visual Studio component). * Tested on Windows Server 2016 with Visual Studio 2017 Community, and Windows Server 2019 with Visual Studio 2022 Community (AppVeyor). * Tested on Windows Server 2022 with Visual Studio 2022 Enterprise (GitHub Actions). diff --git a/winbuild/build.rst b/winbuild/build.rst index 884177d8c..e83045f0c 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -21,7 +21,7 @@ Download and install: `_ (MSVC C++ build tools, and any Windows SDK version required) -* `CMake 3.13 or newer `_ +* `CMake 3.15 or newer `_ (also available as Visual Studio component C++ CMake tools for Windows) * `Ninja `_ From 29b6db4f8a271b0c90d2bf129b40f2f3ca65185d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 6 Mar 2023 10:26:30 +0200 Subject: [PATCH 088/512] 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 089/512] 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 459f0d8352f1bf0394e99b19fd09d529aa928d73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 7 Mar 2023 18:07:46 +1100 Subject: [PATCH 090/512] Round duration when saving animated WebP --- Tests/test_file_webp_animated.py | 12 ++++++++++++ src/PIL/WebPImagePlugin.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index c621df0d9..2fd5e5484 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -134,6 +134,18 @@ def test_timestamp_and_duration(tmp_path): ts += durations[frame] +def test_float_duration(tmp_path): + temp_file = str(tmp_path / "temp.webp") + with Image.open("Tests/images/iss634.apng") as im: + assert im.info["duration"] == 70.0 + + im.save(temp_file, save_all=True) + + with Image.open(temp_file) as reloaded: + reloaded.load() + assert reloaded.info["duration"] == 70 + + def test_seeking(tmp_path): """ Create an animated WebP file, and then try seeking through frames in reverse-order, diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index d060dd4b8..ce8e05fcb 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -285,7 +285,7 @@ def _save_all(im, fp, filename): # Append the frame to the animation encoder enc.add( frame.tobytes("raw", rawmode), - timestamp, + round(timestamp), frame.size[0], frame.size[1], rawmode, @@ -305,7 +305,7 @@ def _save_all(im, fp, filename): im.seek(cur_idx) # Force encoder to flush frames - enc.add(None, timestamp, 0, 0, "", lossless, quality, 0) + enc.add(None, round(timestamp), 0, 0, "", lossless, quality, 0) # Get the final output from the encoder data = enc.assemble(icc_profile, exif, xmp) From 2d01875e7c9be1b2a550f143c542bd931fa7c821 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Mar 2023 13:34:44 +1100 Subject: [PATCH 091/512] 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 092/512] 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 093/512] 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 929dbba834ccbaae1a99c28388e3c223ed9df88f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 17:37:06 +1100 Subject: [PATCH 094/512] Handle failure from PyDict_New or PyList_New --- src/_imaging.c | 6 ++++++ src/_imagingft.c | 9 +++++++++ src/_imagingmorph.c | 7 ++++++- src/path.c | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c..dc14361f6 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1249,6 +1249,9 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); + if (list == NULL) { + return NULL; + } for (i = 0; i < h->bands * 256; i++) { PyObject *item; item = PyLong_FromLong(h->histogram[i]); @@ -2154,6 +2157,9 @@ _getcolors(ImagingObject *self, PyObject *args) { Py_INCREF(out); } else { out = PyList_New(colors); + if (out == NULL) { + return NULL; + } for (i = 0; i < colors; i++) { ImagingColorItem *v = &items[i]; PyObject *item = Py_BuildValue( diff --git a/src/_imagingft.c b/src/_imagingft.c index 0db17a5a6..3e5244b2f 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1082,6 +1082,9 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); + if (list_names == NULL) { + return NULL; + } name_count = FT_Get_Sfnt_Name_Count(self->face); for (i = 0; i < name_count; i++) { @@ -1125,10 +1128,16 @@ font_getvaraxes(FontObject *self) { name_count = FT_Get_Sfnt_Name_Count(self->face); list_axes = PyList_New(num_axis); + if (list_axes == NULL) { + return NULL; + } for (i = 0; i < num_axis; i++) { axis = master->axis[i]; list_axis = PyDict_New(); + if (list_axis == NULL) { + return NULL; + } PyDict_SetItemString( list_axis, "minimum", PyLong_FromLong(axis.minimum / 65536)); PyDict_SetItemString(list_axis, "default", PyLong_FromLong(axis.def / 65536)); diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index c0644b616..43b72539d 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -136,6 +136,9 @@ match(PyObject *self, PyObject *args) { int row_idx, col_idx; UINT8 **inrows; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); @@ -213,10 +216,12 @@ get_on_pixels(PyObject *self, PyObject *args) { int row_idx, col_idx; int width, height; PyObject *ret = PyList_New(0); + if (ret == NULL) { + return NULL; + } if (!PyArg_ParseTuple(args, "n", &i0)) { PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); - return NULL; } img = (Imaging)i0; diff --git a/src/path.c b/src/path.c index 3e3431575..e17580fa2 100644 --- a/src/path.c +++ b/src/path.c @@ -439,6 +439,9 @@ path_tolist(PyPathObject *self, PyObject *args) { if (flat) { list = PyList_New(self->count * 2); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count * 2; i++) { PyObject *item; item = PyFloat_FromDouble(self->xy[i]); @@ -449,6 +452,9 @@ path_tolist(PyPathObject *self, PyObject *args) { } } else { list = PyList_New(self->count); + if (list == NULL) { + return NULL; + } for (i = 0; i < self->count; i++) { PyObject *item; item = Py_BuildValue("dd", self->xy[i + i], self->xy[i + i + 1]); From a334bb6524d38916e39fad44e920ce91cc43eea4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 20:24:34 +1100 Subject: [PATCH 095/512] 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 096/512] 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 097/512] 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 098/512] 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 099/512] 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 b3d782374069d18551d2947008210ab8c53ff072 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 12 Mar 2023 00:11:48 +1100 Subject: [PATCH 100/512] Decrement reference count --- src/_imaging.c | 1 + src/_imagingft.c | 12 ++++++++++++ src/_imagingmorph.c | 5 +++++ src/_imagingtk.c | 6 +++++- src/_webp.c | 1 + 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index dc14361f6..3cc943286 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4306,6 +4306,7 @@ PyInit__imaging(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 3e5244b2f..93866ec4d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1090,6 +1090,7 @@ font_getvarnames(FontObject *self) { for (i = 0; i < name_count; i++) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { + Py_DECREF(list_names); return geterror(error); } @@ -1136,6 +1137,11 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { + for (j = 0; j < i; j++) { + list_axis = PyList_GetItem(list_axes, j); + Py_DECREF(list_axis); + } + Py_DECREF(list_axes); return NULL; } PyDict_SetItemString( @@ -1147,6 +1153,12 @@ font_getvaraxes(FontObject *self) { for (j = 0; j < name_count; j++) { error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + for (j = 0; j < i; j++) { + list_axis = PyList_GetItem(list_axes, j); + Py_DECREF(list_axis); + } + Py_DECREF(list_axes); return geterror(error); } diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 43b72539d..3e0c9172a 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -141,11 +141,13 @@ match(PyObject *self, PyObject *args) { } if (!PyArg_ParseTuple(args, "On", &py_lut, &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } if (!PyBytes_Check(py_lut)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT is not a bytes object"); return NULL; } @@ -153,6 +155,7 @@ match(PyObject *self, PyObject *args) { lut_len = PyBytes_Size(py_lut); if (lut_len < LUT_SIZE) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "The morphology LUT has the wrong size"); return NULL; } @@ -161,6 +164,7 @@ match(PyObject *self, PyObject *args) { imgin = (Imaging)i0; if (imgin->type != IMAGING_TYPE_UINT8 || imgin->bands != 1) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Unsupported image type"); return NULL; } @@ -221,6 +225,7 @@ get_on_pixels(PyObject *self, PyObject *args) { } if (!PyArg_ParseTuple(args, "n", &i0)) { + Py_DECREF(ret); PyErr_SetString(PyExc_RuntimeError, "Argument parsing problem"); return NULL; } diff --git a/src/_imagingtk.c b/src/_imagingtk.c index b9273b0b8..ac6a23138 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -58,5 +58,9 @@ PyInit__imagingtk(void) { }; PyObject *m; m = PyModule_Create(&module_def); - return (load_tkinter_funcs() == 0) ? m : NULL; + if (load_tkinter_funcs() != 0) { + Py_DECREF(m); + return NULL;; + } + return m; } diff --git a/src/_webp.c b/src/_webp.c index 493e0709c..0e38453cb 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -987,6 +987,7 @@ PyInit__webp(void) { m = PyModule_Create(&module_def); if (setup_module(m) < 0) { + Py_DECREF(m); return NULL; } 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 101/512] 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 102/512] 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 103/512] 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 104/512] 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 105/512] 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 106/512] 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 107/512] 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 108/512] 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 109/512] 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 110/512] 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 9e6ae98362c174febada3f8a06d1965524451cf8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 17:31:12 +1100 Subject: [PATCH 111/512] Dropped support for BGR;32 mode --- Tests/test_image.py | 2 +- docs/handbook/concepts.rst | 1 - src/PIL/ImageMode.py | 1 - src/libImaging/Storage.c | 8 -------- 4 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 85e3ff55b..c22628509 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -58,7 +58,7 @@ class TestImage: Image.new(mode, (1, 1)) @pytest.mark.parametrize( - "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24", "BGR;32") + "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24") ) def test_image_modes_fail(self, mode): with pytest.raises(ValueError) as e: diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index 0aa2f1119..e40ed4687 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -62,7 +62,6 @@ Pillow also provides limited support for a few additional modes, including: * ``BGR;15`` (15-bit reversed true colour) * ``BGR;16`` (16-bit reversed true colour) * ``BGR;24`` (24-bit reversed true colour) - * ``BGR;32`` (32-bit reversed true colour) Premultiplied alpha is where the values for each other channel have been multiplied by the alpha. For example, an RGBA pixel of ``(10, 20, 30, 127)`` diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 0973536c9..8b1506e9b 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -61,7 +61,6 @@ def getmode(mode): "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), - "BGR;32": ("RGB", "L", ("B", "G", "R"), endian + "u4"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 76750aaf7..7730b4be8 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -152,14 +152,6 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "BGR;32") == 0) { - /* EXPERIMENTAL */ - /* 32-bit reversed true colour */ - im->bands = 1; - im->pixelsize = 4; - im->linesize = (xsize * 4 + 3) & -4; - im->type = IMAGING_TYPE_SPECIAL; - } else if (strcmp(mode, "RGBX") == 0) { /* 32-bit true colour images with padding */ im->bands = im->pixelsize = 4; From 31669013d420c2a532f5be6c5631827fe36e6f70 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 13 Mar 2023 23:13:34 +1100 Subject: [PATCH 112/512] 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 113/512] 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 114/512] 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 115/512] 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 116/512] 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 117/512] 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 118/512] [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 119/512] 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 0966293a0df0e45a2d4924542e180e917098c2fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Mar 2023 20:05:56 +1100 Subject: [PATCH 120/512] Consider transparency when applying blend mask --- Tests/images/blend_transparency.png | Bin 0 -> 211 bytes Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 10 +++++++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 Tests/images/blend_transparency.png diff --git a/Tests/images/blend_transparency.png b/Tests/images/blend_transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..cef0a16de1f2db2db74e87d8bed5c8ba32038f28 GIT binary patch literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryY$ZW{!9e;y1H%P7;~F4~BQZI| z2S|eyF?{ZwTmYn`(ja0WA+Ql31_Us_U|?Kvy6GB_$K&bZ7*a7O`N#PK3=GVSj0`9L zO?nR$hv|mt1kq3c(rvsW17w##T8d)`kY)m!knrO?kio>rAZ@=c0i@K^)z4*}Q$iB} Do3|(? literal 0 HcmV?d00001 diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index b2bec5984..feca72aa6 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -163,6 +163,12 @@ def test_apng_blend(): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_blend_transparency(): + with Image.open("Tests/images/blend_transparency.png") as im: + im.seek(1) + assert im.getpixel((0, 0)) == (255, 0, 0) + + def test_apng_chunk_order(): with Image.open("Tests/images/apng/fctl_actl.png") as im: im.seek(im.n_frames - 1) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 9078957dc..15a3c8291 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1003,9 +1003,13 @@ class PngImageFile(ImageFile.ImageFile): else: if self._prev_im and self.blend_op == Blend.OP_OVER: updated = self._crop(self.im, self.dispose_extent) - self._prev_im.paste( - updated, self.dispose_extent, updated.convert("RGBA") - ) + if self.im.mode == "RGB" and "transparency" in self.info: + mask = updated.convert_transparent( + "RGBA", self.info["transparency"] + ) + else: + mask = updated.convert("RGBA") + self._prev_im.paste(updated, self.dispose_extent, mask) self.im = self._prev_im if self.pyaccess: self.pyaccess = None From 5080d3581694d1a73a38845d17630808dd3fea10 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Mar 2023 11:27:58 +1100 Subject: [PATCH 121/512] 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 80edcd18d162793d1e4dc7a87952bdedcee2ee5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 17 Mar 2023 20:11:35 +1100 Subject: [PATCH 122/512] Do not render text if image has zero width or height --- src/PIL/ImageFont.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 30f6694e6..b08f10790 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -767,18 +767,19 @@ class FreeTypeFont: offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) im = fill("RGBA" if mode == "RGBA" else "L", size, 0) - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) + if min(size): + self.font.render( + text, + im.id, + mode, + direction, + features, + language, + stroke_width, + ink, + start[0], + start[1], + ) return im, offset def font_variant( 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 123/512] 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 c7d4d1f75a8cee77641629e5a9bf4408146188a6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 18 Mar 2023 22:47:04 +1100 Subject: [PATCH 124/512] Fixed typo Co-authored-by: Aarni Koskela --- src/_imagingtk.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingtk.c b/src/_imagingtk.c index ac6a23138..efa7fc1b6 100644 --- a/src/_imagingtk.c +++ b/src/_imagingtk.c @@ -60,7 +60,7 @@ PyInit__imagingtk(void) { m = PyModule_Create(&module_def); if (load_tkinter_funcs() != 0) { Py_DECREF(m); - return NULL;; + return NULL; } return m; } 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 125/512] 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 126/512] 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 127/512] 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 128/512] 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 01cdfb6b27ed857fc63e625514a97352c5dc89cf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 19 Mar 2023 23:20:54 +1100 Subject: [PATCH 129/512] Removed duplicate calls to PyTuple_GET_SIZE --- src/_imaging.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 1c25ab00c..e7e403c95 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -491,7 +491,7 @@ getink(PyObject *color, Imaging im, char *ink) { int g = 0, b = 0, a = 0; double f = 0; /* Windows 64 bit longs are 32 bits, and 0xFFFFFFFF (white) is a - python long (not int) that raises an overflow error when trying + Python long (not int) that raises an overflow error when trying to return it into a 32 bit C long */ PY_LONG_LONG r = 0; @@ -502,8 +502,12 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - if (PyTuple_Check(color) && PyTuple_GET_SIZE(color) == 1) { - color = PyTuple_GetItem(color, 0); + int tupleSize; + if (PyTuple_Check(color)) { + tupleSize = PyTuple_GET_SIZE(color); + if (tupleSize == 1) { + color = PyTuple_GetItem(color, 0); + } } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { @@ -531,7 +535,7 @@ getink(PyObject *color, Imaging im, char *ink) { if (im->bands == 1) { /* unsigned integer, single layer */ if (rIsInt != 1) { - if (PyTuple_GET_SIZE(color) != 1) { + if (tupleSize != 1) { PyErr_SetString(PyExc_TypeError, "color must be int or single-element tuple"); return NULL; } else if (!PyArg_ParseTuple(color, "L", &r)) { @@ -541,7 +545,6 @@ getink(PyObject *color, Imaging im, char *ink) { ink[0] = (char)CLIP8(r); ink[1] = ink[2] = ink[3] = 0; } else { - a = 255; if (rIsInt) { /* compatibility: ABGR */ a = (UINT8)(r >> 24); @@ -549,7 +552,7 @@ getink(PyObject *color, Imaging im, char *ink) { g = (UINT8)(r >> 8); r = (UINT8)r; } else { - int tupleSize = PyTuple_GET_SIZE(color); + a = 255; if (im->bands == 2) { if (tupleSize != 1 && tupleSize != 2) { PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or two elements"); From 11d100ce5deb6028ca91633e75a01d648dc936c5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 00:30:10 +1100 Subject: [PATCH 130/512] Support creating BGR;15, BGR;16 and BGR;24 images --- Tests/test_image.py | 7 +++--- Tests/test_image_access.py | 14 ++++++------ src/PIL/ImageMode.py | 6 ++--- src/_imaging.c | 45 +++++++++++++++++++++++++++++++++----- src/libImaging/Storage.c | 6 ++--- 5 files changed, 56 insertions(+), 22 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index c22628509..17f1edb00 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -48,6 +48,9 @@ class TestImage: "RGBX", "RGBA", "RGBa", + "BGR;15", + "BGR;16", + "BGR;24", "CMYK", "YCbCr", "LAB", @@ -57,9 +60,7 @@ class TestImage: def test_image_modes_success(self, mode): Image.new(mode, (1, 1)) - @pytest.mark.parametrize( - "mode", ("", "bad", "very very long", "BGR;15", "BGR;16", "BGR;24") - ) + @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode): with pytest.raises(ValueError) as e: Image.new(mode, (1, 1)) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 4079d9358..29eed745e 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -363,8 +363,8 @@ class TestCffi(AccessTest): class TestImagePutPixelError(AccessTest): - IMAGE_MODES1 = ["L", "LA", "RGB", "RGBA"] - IMAGE_MODES2 = ["I", "I;16", "BGR;15"] + IMAGE_MODES1 = ["LA", "RGB", "RGBA", "BGR;15"] + IMAGE_MODES2 = ["L", "I", "I;16"] INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) @@ -379,6 +379,11 @@ class TestImagePutPixelError(AccessTest): ( ("L", (0, 2), "color must be int or single-element tuple"), ("LA", (0, 3), "color must be int, or tuple of one or two elements"), + ( + "BGR;15", + (0, 2), + "color must be int, or tuple of one or three elements", + ), ( "RGB", (0, 2, 5), @@ -407,11 +412,6 @@ class TestImagePutPixelError(AccessTest): with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) - def test_putpixel_unrecognized_mode(self): - im = hopper("BGR;15") - with pytest.raises(ValueError, match="unrecognized image mode"): - im.putpixel((0, 0), 0) - class TestEmbeddable: @pytest.mark.xfail(reason="failing test") diff --git a/src/PIL/ImageMode.py b/src/PIL/ImageMode.py index 8b1506e9b..a0b335142 100644 --- a/src/PIL/ImageMode.py +++ b/src/PIL/ImageMode.py @@ -58,9 +58,9 @@ def getmode(mode): "HSV": ("RGB", "L", ("H", "S", "V"), "|u1"), # extra experimental modes "RGBa": ("RGB", "L", ("R", "G", "B", "a"), "|u1"), - "BGR;15": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;16": ("RGB", "L", ("B", "G", "R"), endian + "u2"), - "BGR;24": ("RGB", "L", ("B", "G", "R"), endian + "u3"), + "BGR;15": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;16": ("RGB", "L", ("B", "G", "R"), "|u1"), + "BGR;24": ("RGB", "L", ("B", "G", "R"), "|u1"), "LA": ("L", "L", ("L", "A"), "|u1"), "La": ("L", "L", ("L", "a"), "|u1"), "PA": ("RGB", "L", ("P", "A"), "|u1"), diff --git a/src/_imaging.c b/src/_imaging.c index e7e403c95..3b17d638e 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -517,15 +517,13 @@ getink(PyObject *color, Imaging im, char *ink) { return NULL; } rIsInt = 1; - } else if (im->type == IMAGING_TYPE_UINT8) { - if (!PyTuple_Check(color)) { - PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); - return NULL; - } - } else { + } else if (im->bands == 1) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; + } else if (!PyTuple_Check(color)) { + PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); + return NULL; } } @@ -596,6 +594,41 @@ getink(PyObject *color, Imaging im, char *ink) { ink[1] = (UINT8)(r >> 8); ink[2] = ink[3] = 0; return ink; + } else { + if (rIsInt) { + b = (UINT8)(r >> 16); + g = (UINT8)(r >> 8); + r = (UINT8)r; + } else if (tupleSize != 3) { + PyErr_SetString(PyExc_TypeError, "color must be int, or tuple of one or three elements"); + return NULL; + } else if (!PyArg_ParseTuple(color, "Lii", &r, &g, &b)) { + return NULL; + } + if (!strcmp(im->mode, "BGR;15")) { + UINT16 v = ((((UINT16)r) << 7) & 0x7c00) + + ((((UINT16)g) << 2) & 0x03e0) + + ((((UINT16)b) >> 3) & 0x001f); + + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;16")) { + UINT16 v = ((((UINT16)r) << 8) & 0xf800) + + ((((UINT16)g) << 3) & 0x07e0) + + ((((UINT16)b) >> 3) & 0x001f); + ink[0] = (UINT8)v; + ink[1] = (UINT8)(v >> 8); + ink[2] = ink[3] = 0; + return ink; + } else if (!strcmp(im->mode, "BGR;24")) { + ink[0] = (UINT8)b; + ink[1] = (UINT8)g; + ink[2] = (UINT8)r; + ink[3] = 0; + return ink; + } } } diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7730b4be8..7cf00ef35 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -131,7 +131,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;15") == 0) { /* EXPERIMENTAL */ /* 15-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -139,7 +139,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;16") == 0) { /* EXPERIMENTAL */ /* 16-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 2; im->linesize = (xsize * 2 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; @@ -147,7 +147,7 @@ ImagingNewPrologueSubtype(const char *mode, int xsize, int ysize, int size) { } else if (strcmp(mode, "BGR;24") == 0) { /* EXPERIMENTAL */ /* 24-bit reversed true colour */ - im->bands = 1; + im->bands = 3; im->pixelsize = 3; im->linesize = (xsize * 3 + 3) & -4; im->type = IMAGING_TYPE_SPECIAL; From 63286622488ffaeb481f74a0c499fd8732df98e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 17:34:34 +1100 Subject: [PATCH 131/512] 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 dfeed0eb7ea1f6a89921f9a2392c111740884ce3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 20 Mar 2023 22:44:14 +1100 Subject: [PATCH 132/512] Group decrementing reference counts for previous axes in font_getvaraxes --- src/_imagingft.c | 49 +++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 93866ec4d..bf9a24287 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1114,7 +1114,7 @@ font_getvarnames(FontObject *self) { static PyObject * font_getvaraxes(FontObject *self) { - int error; + int error, failed = 0; FT_UInt i, j, num_axis, name_count; FT_MM_Var *master; FT_Var_Axis axis; @@ -1137,36 +1137,39 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { + failed = 1; + } else { + 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)); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); + if (error) { + Py_DECREF(list_axis); + failed = 1; + break; + } + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name); + break; + } + } + } + if (failed) { for (j = 0; j < i; j++) { list_axis = PyList_GetItem(list_axes, j); Py_DECREF(list_axis); } Py_DECREF(list_axes); - return NULL; - } - 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)); - - for (j = 0; j < name_count; j++) { - error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { - Py_DECREF(list_axis); - for (j = 0; j < i; j++) { - list_axis = PyList_GetItem(list_axes, j); - Py_DECREF(list_axis); - } - Py_DECREF(list_axes); return geterror(error); } - - if (name.name_id == axis.strid) { - axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name); - break; - } + return NULL; } PyList_SetItem(list_axes, i, list_axis); From 76d36da12e8b17d7e91814056c88bea5b0c614fa Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 00:59:00 +0000 Subject: [PATCH 133/512] 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 134/512] 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 135/512] 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 136/512] 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 137/512] 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 138/512] 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] From 415455d01b1808e7b65ba88908dc33d0c5172ce2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 09:05:09 +1100 Subject: [PATCH 139/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 706d9ad76..09462a264 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added reading of JPEG2000 comments #6909 + [radarhere] + +- Decrement reference count #7003 + [radarhere, nulano] + - Allow libtiff_support_custom_tags to be missing #7020 [radarhere] From 6a931861fe45196754a0de2cf8f96c5821d62964 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 10:12:18 +1100 Subject: [PATCH 140/512] Load before getting size in __array_interface__ --- Tests/test_numpy.py | 9 ++++++++- src/PIL/Image.py | 7 ++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index a8bbcbdb8..147f94a71 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import assert_deep_equal, assert_image, hopper +from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature numpy = pytest.importorskip("numpy", reason="NumPy not installed") @@ -219,6 +219,13 @@ def test_zero_size(): assert im.size == (0, 0) +@skip_unless_feature("libtiff") +def test_load_first(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + a = numpy.array(im) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cf9ab2df6..95f5a9bc1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -686,11 +686,7 @@ class Image: @property def __array_interface__(self): # numpy array interface support - new = {} - shape, typestr = _conv_type_shape(self) - new["shape"] = shape - new["typestr"] = typestr - new["version"] = 3 + new = {"version": 3} try: if self.mode == "1": # Binary images need to be extended from bits to bytes @@ -709,6 +705,7 @@ class Image: if parse_version(numpy.__version__) < parse_version("1.23"): warnings.warn(e) raise + new["shape"], new["typestr"] = _conv_type_shape(self) return new def __getstate__(self): From d3923f71420c58f8545fc74fc5f41f0b415635f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 23 Mar 2023 17:53:35 +1100 Subject: [PATCH 141/512] Use reading of comments to test saving comments --- Tests/test_file_jpeg2k.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 8261c612a..a869d74f0 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -372,6 +372,20 @@ def test_comment(): pass +def test_save_comment(): + comment = "Created by Pillow" + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) + + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" + + too_long_comment = " " * 65532 + with pytest.raises(ValueError): + test_card.save(out, "JPEG2000", comment=too_long_comment) + + @pytest.mark.parametrize( "test_file", [ @@ -391,20 +405,6 @@ def test_crashes(test_file): pass -def test_custom_comment(): - output_stream = BytesIO() - unique_comment = "This is a unique comment, which should be found below" - test_card.save(output_stream, "JPEG2000", comment=unique_comment) - output_stream.seek(0) - data = output_stream.read() - # Lazy method to determine if the comment is in the image generated - assert bytes(unique_comment, "utf-8") in data - - too_long_comment = " " * 65532 - with pytest.raises(ValueError): - test_card.save(output_stream, "JPEG2000", comment=too_long_comment) - - @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) From 7c3fd254330ebcbd51fa08ef0b709b52587f5b92 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Mar 2023 09:45:51 +1100 Subject: [PATCH 142/512] Allow saving bytes as comments --- Tests/test_file_jpeg2k.py | 14 +++++++------- src/PIL/Jpeg2KImagePlugin.py | 4 +++- src/encode.c | 21 +++++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a869d74f0..60be50e07 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -373,15 +373,15 @@ def test_comment(): def test_save_comment(): - comment = "Created by Pillow" - out = BytesIO() - test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) + for comment in ("Created by Pillow", b"Created by Pillow"): + out = BytesIO() + test_card.save(out, "JPEG2000", comment=comment) + out.seek(0) - with Image.open(out) as im: - assert im.info["comment"] == b"Created by Pillow" + with Image.open(out) as im: + assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65532 + too_long_comment = " " * 65531 with pytest.raises(ValueError): test_card.save(out, "JPEG2000", comment=too_long_comment) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 4249fe714..980c299db 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -351,10 +351,12 @@ def _save(im, fp, filename): cinema_mode = info.get("cinema_mode", "no") mct = info.get("mct", 0) signed = info.get("signed", False) - fd = -1 comment = info.get("comment") + if isinstance(comment, str): + comment = comment.encode() add_plt = info.get("add_plt", False) + fd = -1 if hasattr(fp, "fileno"): try: fd = fp.fileno() diff --git a/src/encode.c b/src/encode.c index e8946dbae..7dcb79766 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1215,11 +1215,12 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { int sgnd = 0; Py_ssize_t fd = -1; char *comment = NULL; + Py_ssize_t comment_size; int add_plt = 0; if (!PyArg_ParseTuple( args, - "ss|OOOsOnOOOssbbnzp", + "ss|OOOsOnOOOssbbnz#p", &mode, &format, &offset, @@ -1237,6 +1238,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &sgnd, &fd, &comment, + &comment_size, &add_plt)) { return NULL; } @@ -1319,9 +1321,9 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { } } - if (comment != NULL && strlen(comment) > 0) { + if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (strlen(comment) >= 65531) { + if (comment_size >= 65531) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); @@ -1329,15 +1331,14 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { return NULL; } - context->comment = strdup(comment); - - if (context->comment == NULL) { - PyErr_SetString( - PyExc_MemoryError, - "Couldn't allocate memory for JPEG 2000 comment"); + char *p = malloc(comment_size + 1); + if (!p) { Py_DECREF(encoder); - return NULL; + return ImagingError_MemoryError(); } + memcpy(p, comment, comment_size); + p[comment_size] = '\0'; + context->comment = p; } if (quality_layers && PySequence_Check(quality_layers)) { From 311ab716e055cbb90cf5415138893d0bab99a357 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Mar 2023 09:48:25 +1100 Subject: [PATCH 143/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 09462a264..6861c61a1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Consider transparency when applying APNG blend mask #7018 + [radarhere] + +- Round duration when saving animated WebP images #6996 + [radarhere] + - Added reading of JPEG2000 comments #6909 [radarhere] From ce1acb9a75bb96862db5fbdafbd9b8e129f71d71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 24 Mar 2023 03:50:20 +0000 Subject: [PATCH 144/512] Update actions/stale action to v8 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 8c210bc90..24b8f85d1 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,7 +20,7 @@ jobs: steps: - name: "Check issues" - uses: actions/stale@v7 + uses: actions/stale@v8 with: repo-token: ${{ secrets.GITHUB_TOKEN }} only-labels: "Awaiting OP Action" From 5e7c1801e0a31d26a10988449f208f9cf26c43f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Mar 2023 11:32:22 +1100 Subject: [PATCH 145/512] Install Ghostscript using Chocolatey --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index d4dd2dc95..b5913e043 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -27,8 +27,8 @@ install: - mv c:\pillow-depends-main c:\pillow-depends - 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% +- choco install ghostscript --version=10.0.0.20230317 +- path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 833f096c3..109e8d058 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -74,8 +74,8 @@ jobs: 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH - winbuild\depends\gs1000w32.exe /S - echo "C:\Program Files (x86)\gs\gs10.0.0\bin" >> $env:GITHUB_PATH + choco install ghostscript --version=10.0.0.20230317 + echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images From a236c272a801b7d7feab591412745656b1b88fc4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 25 Mar 2023 23:41:27 +1100 Subject: [PATCH 146/512] Added release notes --- docs/releasenotes/9.5.0.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bd6e58693..849e87594 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -63,3 +63,13 @@ Added support for saving PDFs in RGBA mode ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using the JPXDecode filter, PDFs can now be saved in RGBA mode. + +BGR;* modes +^^^^^^^^^^^ + +It is now possible to create new BGR;15, BGR;16 and BGR;24 images. Conversely, BGR;32 +has been removed from ImageMode and its associated methods, dropping the little support +Pillow had for the mode. + +With that, all modes listed under :ref:`concept-modes` can now be used to create a new +image. From bdcc6333b697f82ae12139385f2c02f8e4d8b0fd Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 26 Mar 2023 08:34:42 +1100 Subject: [PATCH 147/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6861c61a1..bbe47473d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Load before getting size in __array_interface__ #7034 + [radarhere] + +- Support creating BGR;15, BGR;16 and BGR;24 images, but drop support for BGR;32 #7010 + [radarhere] + - Consider transparency when applying APNG blend mask #7018 [radarhere] From 1fd189164c8905d2f99f0d9836ef7ea4c897b055 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:38:29 +1100 Subject: [PATCH 148/512] Renamed "add_plt" to "plt" --- Tests/test_file_jpeg2k.py | 2 +- src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/encode.c | 6 +++--- src/libImaging/Jpeg2K.h | 2 +- src/libImaging/Jpeg2KEncode.c | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 60be50e07..7b512695b 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -409,7 +409,7 @@ def test_crashes(test_file): def test_plt_marker(): # Search the start of the codesteam for the PLT box (id 0xFF58) out = BytesIO() - test_card.save(out, "JPEG2000", no_jp2=True, add_plt=True) + test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: box_bytes = out.read(2) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 980c299db..e7d91c818 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -354,7 +354,7 @@ def _save(im, fp, filename): comment = info.get("comment") if isinstance(comment, str): comment = comment.encode() - add_plt = info.get("add_plt", False) + plt = info.get("plt", False) fd = -1 if hasattr(fp, "fileno"): @@ -379,7 +379,7 @@ def _save(im, fp, filename): signed, fd, comment, - add_plt, + plt, ) ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) diff --git a/src/encode.c b/src/encode.c index 7dcb79766..8aa357b6c 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1216,7 +1216,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { Py_ssize_t fd = -1; char *comment = NULL; Py_ssize_t comment_size; - int add_plt = 0; + int plt = 0; if (!PyArg_ParseTuple( args, @@ -1239,7 +1239,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { &fd, &comment, &comment_size, - &add_plt)) { + &plt)) { return NULL; } @@ -1358,7 +1358,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { context->cinema_mode = cine_mode; context->mct = mct; context->sgnd = sgnd; - context->add_plt = add_plt; + context->plt = plt; return (PyObject *)encoder; } diff --git a/src/libImaging/Jpeg2K.h b/src/libImaging/Jpeg2K.h index 7bf8b4b0a..e8d92f7b6 100644 --- a/src/libImaging/Jpeg2K.h +++ b/src/libImaging/Jpeg2K.h @@ -101,7 +101,7 @@ typedef struct { char *comment; /* Include PLT marker segment */ - int add_plt; + int plt; } JPEG2KENCODESTATE; diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 6d6add76e..a7c644197 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -502,7 +502,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) - if (context->add_plt) { + if (context->plt) { const char * plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } From a7df096d1b03d53c9834989e3e2e5e4fb8b37f4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 27 Mar 2023 22:39:01 +1100 Subject: [PATCH 149/512] Added release notes --- docs/releasenotes/9.5.0.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 074931671..20d7ee893 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -48,11 +48,16 @@ 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 -^^^^^^^^^^^^^^^^^^^^^ +JPEG2000 comments and PLT marker +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When opening a JPEG2000 image, the comment may now be read into -:py:attr:`~PIL.Image.Image.info`. +:py:attr:`~PIL.Image.Image.info`. The ``comment`` keyword argument can be used +to save it back again. + +If OpenJPEG 2.4.0 or later is available and the ``plt`` keyword argument +is present and true when saving JPEG2000 images, tell the encoder to generate +PLT markers. Security ======== From 7d6ff23e1f493147057f062f95954b21e9eec0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 28 Mar 2023 07:32:30 +1100 Subject: [PATCH 150/512] Renamed "add_plt" to "plt" 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 11380cd55..de6b79371 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -595,7 +595,7 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 -**add_plt** +**plt** If ``True`` then include a PLT (packet length, tile-part header) marker segment in the produced file. The default is to not include it. From 598216fb465682646a307b930047d56adb0a42b7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:18:50 +1100 Subject: [PATCH 151/512] OpenJPEG 2.4.0 or later is required for PLT markers --- docs/handbook/image-file-formats.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index de6b79371..74ba883b1 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -596,9 +596,9 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: .. versionadded:: 9.5.0 **plt** - If ``True`` then include a PLT (packet length, tile-part header) marker - segment in the produced file. - The default is to not include it. + If ``True`` and OpenJPEG 2.4.0 or later is available, then include a PLT + (packet length, tile-part header) marker in the produced file. + Defaults to ``False``. .. versionadded:: 9.5.0 From 2f66d2d6a1dd307e8c28ddc5f98707acc5874cdb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 19:19:05 +1100 Subject: [PATCH 152/512] Changed maximum comment length to 65531 --- Tests/test_file_jpeg2k.py | 10 +++++++--- src/encode.c | 4 ++-- src/libImaging/Jpeg2KEncode.c | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 7b512695b..b9422c76a 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -376,14 +376,18 @@ def test_save_comment(): for comment in ("Created by Pillow", b"Created by Pillow"): out = BytesIO() test_card.save(out, "JPEG2000", comment=comment) - out.seek(0) with Image.open(out) as im: assert im.info["comment"] == b"Created by Pillow" - too_long_comment = " " * 65531 + out = BytesIO() + long_comment = b" " * 65531 + test_card.save(out, "JPEG2000", comment=long_comment) + with Image.open(out) as im: + assert im.info["comment"] == long_comment + with pytest.raises(ValueError): - test_card.save(out, "JPEG2000", comment=too_long_comment) + test_card.save(out, "JPEG2000", comment=long_comment + b" ") @pytest.mark.parametrize( diff --git a/src/encode.c b/src/encode.c index 8aa357b6c..a66594935 100644 --- a/src/encode.c +++ b/src/encode.c @@ -1214,7 +1214,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { char mct = 0; int sgnd = 0; Py_ssize_t fd = -1; - char *comment = NULL; + char *comment; Py_ssize_t comment_size; int plt = 0; @@ -1323,7 +1323,7 @@ PyImaging_Jpeg2KEncoderNew(PyObject *self, PyObject *args) { if (comment && comment_size > 0) { /* Size is stored as as an uint16, subtract 4 bytes for the header */ - if (comment_size >= 65531) { + if (comment_size >= 65532) { PyErr_SetString( PyExc_ValueError, "JPEG 2000 comment is too long"); diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index a7c644197..8f6370061 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -503,7 +503,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { /* Enabling PLT markers only supported in OpenJPEG 2.4.0 and up */ #if ((OPJ_VERSION_MAJOR == 2 && OPJ_VERSION_MINOR >= 4) || OPJ_VERSION_MAJOR > 2) if (context->plt) { - const char * plt_option[2] = {"PLT=YES", NULL}; + const char *plt_option[2] = {"PLT=YES", NULL}; opj_encoder_set_extra_options(codec, plt_option); } #endif From 9a7a4482195125f38c668df3555043e8d3251da0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 20:14:29 +1100 Subject: [PATCH 153/512] Increase similiarity between test_plt_marker and _parse_comment --- Tests/test_file_jpeg2k.py | 21 +++++++++++---------- src/PIL/Jpeg2KImagePlugin.py | 8 ++++---- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b9422c76a..52e9f8853 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -411,29 +411,30 @@ def test_crashes(test_file): @skip_unless_feature_version("jpg_2000", "2.4.0") def test_plt_marker(): - # Search the start of the codesteam for the PLT box (id 0xFF58) + # Search the start of the codesteam for PLT out = BytesIO() test_card.save(out, "JPEG2000", no_jp2=True, plt=True) out.seek(0) while True: - box_bytes = out.read(2) - if not box_bytes: + marker = out.read(2) + if not marker: # End of steam encountered and no PLT or SOD break - jp2_boxid = _binary.i16be(box_bytes) + jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: - # No length specifier for main header + # SOC has no length continue elif jp2_boxid == 0xFF58: - # This is the PLT box we're looking for + # PLT return elif jp2_boxid == 0xFF93: - # SOD box encountered and no PLT, so it wasn't found + # SOD without finding PLT first break - jp2_boxlength = _binary.i16be(out.read(2)) - out.seek(jp2_boxlength - 2, os.SEEK_CUR) + hdr = out.read(2) + length = _binary.i16be(hdr) + out.seek(length - 2, os.SEEK_CUR) - # The PLT box wasn't found + # PLT wasn't found raise ValueError diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index e7d91c818..9309768ba 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -17,7 +17,7 @@ import io import os import struct -from . import Image, ImageFile +from . import Image, ImageFile, _binary class BoxReader: @@ -99,7 +99,7 @@ def _parse_codestream(fp): count from the SIZ marker segment, returning a PIL (size, mode) tuple.""" hdr = fp.read(2) - lsiz = struct.unpack(">H", hdr)[0] + lsiz = _binary.i16be(hdr) siz = hdr + fp.read(lsiz - 2) lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack_from( ">HHIIIIIIIIH", siz @@ -258,7 +258,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): def _parse_comment(self): hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) while True: @@ -270,7 +270,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): # Start of tile or end of codestream break hdr = self.fp.read(2) - length = struct.unpack(">H", hdr)[0] + length = _binary.i16be(hdr) if typ == 0x64: # Comment self.info["comment"] = self.fp.read(length - 2)[2:] From 6d3c1985e07dfa28ae14de5bf36c711bdfc9176d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 22:18:14 +1100 Subject: [PATCH 154/512] Assert false instead of raising an error --- Tests/test_file_jpeg2k.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 52e9f8853..b6e8215f7 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -418,8 +418,7 @@ def test_plt_marker(): while True: marker = out.read(2) if not marker: - # End of steam encountered and no PLT or SOD - break + assert False, "End of stream without PLT" jp2_boxid = _binary.i16be(marker) if jp2_boxid == 0xFF4F: @@ -429,12 +428,8 @@ def test_plt_marker(): # PLT return elif jp2_boxid == 0xFF93: - # SOD without finding PLT first - break + assert False, "SOD without finding PLT first" hdr = out.read(2) length = _binary.i16be(hdr) out.seek(length - 2, os.SEEK_CUR) - - # PLT wasn't found - raise ValueError From d7dd44dde01ea0f98073197f07a07915166733d0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 29 Mar 2023 23:46:06 +1100 Subject: [PATCH 155/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bbe47473d..969f4be08 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Support custom comments and PLT markers when saving JPEG2000 images #6903 + [joshware, radarhere, hugovk] + - Load before getting size in __array_interface__ #7034 [radarhere] From 61d0c8f5230a3fdde4c601a73217acd386d1b3be Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 29 Mar 2023 10:30:20 -0500 Subject: [PATCH 156/512] change PSFile deprecation from 9.4.0 to 9.5.0 --- docs/deprecations.rst | 20 ++++++++++---------- docs/releasenotes/9.4.0.rst | 11 ----------- docs/releasenotes/9.5.0.rst | 9 ++++++--- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4a03890a3..5669d2827 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -80,16 +80,6 @@ A number of constants have been deprecated and will be removed in Pillow 10.0.0 was reversed in Pillow 9.4.0 and those constants will now remain available. See :ref:`restored-image-constants` -PSFile -~~~~~~ - -.. deprecated:: 9.4.0 - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - ===================================================== ============================================================ Deprecated Use instead ===================================================== ============================================================ @@ -217,6 +207,16 @@ Use instead:: left, top, right, bottom = draw.multiline_textbbox((0, 0), "Hello\nworld") width, height = right - left, bottom - top +PSFile +~~~~~~ + +.. deprecated:: 9.5.0 + +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. + Removed features ---------------- diff --git a/docs/releasenotes/9.4.0.rst b/docs/releasenotes/9.4.0.rst index b7a63dd61..0af5bc8ca 100644 --- a/docs/releasenotes/9.4.0.rst +++ b/docs/releasenotes/9.4.0.rst @@ -1,17 +1,6 @@ 9.4.0 ----- -Deprecations -============ - -PSFile -^^^^^^ - -The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will -be removed in Pillow 11 (2024-10-15). This class was only made as a helper to -be used internally, so there is no replacement. If you need this functionality -though, it is a very short class that can easily be recreated in your own code. - API Additions ============= diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 0b0e0dd2f..585e790ea 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -12,10 +12,13 @@ TODO Deprecations ============ -TODO -^^^^ +PSFile +^^^^^^ -TODO +The :py:class:`~PIL.EpsImagePlugin.PSFile` class has been deprecated and will +be removed in Pillow 11 (2024-10-15). This class was only made as a helper to +be used internally, so there is no replacement. If you need this functionality +though, it is a very short class that can easily be recreated in your own code. API Changes =========== From 0ea1184bcfefd1965670cf6d06ca9d44364460d2 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 30 Mar 2023 07:54:01 +1100 Subject: [PATCH 157/512] Free additional variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/_imaging.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_imaging.c b/src/_imaging.c index c715c36c2..b6a7557ff 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1250,6 +1250,7 @@ _histogram(ImagingObject *self, PyObject *args) { /* Build an integer list containing the histogram */ list = PyList_New(h->bands * 256); if (list == NULL) { + ImagingHistogramDelete(h); return NULL; } for (i = 0; i < h->bands * 256; i++) { @@ -2158,6 +2159,7 @@ _getcolors(ImagingObject *self, PyObject *args) { } else { out = PyList_New(colors); if (out == NULL) { + free(items); return NULL; } for (i = 0; i < colors; i++) { From 7632d8df3642111bd1b20b0863511a2ff6b3a113 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Mar 2023 12:35:07 +1100 Subject: [PATCH 158/512] Do not DECREF individual list items, reverting grouping --- src/_imagingft.c | 63 ++++++++++++++++++++---------------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 57f765e13..92cfb1db0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1114,7 +1114,7 @@ font_getvarnames(FontObject *self) { static PyObject * font_getvaraxes(FontObject *self) { - int error, failed = 0; + int error; FT_UInt i, j, num_axis, name_count; FT_MM_Var *master; FT_Var_Axis axis; @@ -1137,46 +1137,35 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { - failed = 1; - } else { - PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); - 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 ? def : Py_None); - Py_XDECREF(def); - - PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); - PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); - Py_XDECREF(maximum); - - for (j = 0; j < name_count; j++) { - error = FT_Get_Sfnt_Name(self->face, j, &name); - if (error) { - Py_DECREF(list_axis); - failed = 1; - break; - } - - if (name.name_id == axis.strid) { - axis_name = Py_BuildValue("y#", name.string, name.string_len); - PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); - Py_XDECREF(axis_name); - break; - } - } - } - if (failed) { - for (j = 0; j < i; j++) { - list_axis = PyList_GetItem(list_axes, j); - Py_DECREF(list_axis); - } Py_DECREF(list_axes); + return NULL; + } + PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); + 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 ? def : Py_None); + Py_XDECREF(def); + + PyObject *maximum = PyLong_FromLong(axis.maximum / 65536); + PyDict_SetItemString(list_axis, "maximum", maximum ? maximum : Py_None); + Py_XDECREF(maximum); + + for (j = 0; j < name_count; j++) { + error = FT_Get_Sfnt_Name(self->face, j, &name); if (error) { + Py_DECREF(list_axis); + Py_DECREF(list_axes); return geterror(error); } - return NULL; + + if (name.name_id == axis.strid) { + axis_name = Py_BuildValue("y#", name.string, name.string_len); + PyDict_SetItemString(list_axis, "name", axis_name ? axis_name : Py_None); + Py_XDECREF(axis_name); + break; + } } PyList_SetItem(list_axes, i, list_axis); From 448ab0a68780a32b754eb1edb42b4b45d85491af Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 30 Mar 2023 14:36:58 +1100 Subject: [PATCH 159/512] Call FT_Done_MM_Var when returning early MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/_imagingft.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 92cfb1db0..e0c289865 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1083,6 +1083,7 @@ font_getvarnames(FontObject *self) { num_namedstyles = master->num_namedstyles; list_names = PyList_New(num_namedstyles); if (list_names == NULL) { + FT_Done_MM_Var(library, master); return NULL; } @@ -1091,6 +1092,7 @@ font_getvarnames(FontObject *self) { error = FT_Get_Sfnt_Name(self->face, i, &name); if (error) { Py_DECREF(list_names); + FT_Done_MM_Var(library, master); return geterror(error); } @@ -1130,6 +1132,7 @@ font_getvaraxes(FontObject *self) { list_axes = PyList_New(num_axis); if (list_axes == NULL) { + FT_Done_MM_Var(library, master); return NULL; } for (i = 0; i < num_axis; i++) { @@ -1138,6 +1141,7 @@ font_getvaraxes(FontObject *self) { list_axis = PyDict_New(); if (list_axis == NULL) { Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return NULL; } PyObject *minimum = PyLong_FromLong(axis.minimum / 65536); @@ -1157,6 +1161,7 @@ font_getvaraxes(FontObject *self) { if (error) { Py_DECREF(list_axis); Py_DECREF(list_axes); + FT_Done_MM_Var(library, master); return geterror(error); } From c3364a424516b365bc2504b24faa5d223066ee26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 30 Mar 2023 16:55:18 +1100 Subject: [PATCH 160/512] Do not use absolute path for ldconfig --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f8bcf3e39..9f38170b5 100755 --- a/setup.py +++ b/setup.py @@ -166,14 +166,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["/sbin/ldconfig", "-p"] + args = ["ldconfig", "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["/sbin/ldconfig", "-r"] + args = ["ldconfig", "-r"] expr = r".* => (.*)" env = {} From 17a0a2ee3eeb9df6e9fcf894d204911c7e6e4eef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 06:14:35 +1100 Subject: [PATCH 161/512] Removed unnecessary silencing of stderr --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9f38170b5..07d6c66d6 100755 --- a/setup.py +++ b/setup.py @@ -273,9 +273,7 @@ def _pkg_config(name): )[::2][1:] cflags = re.split( r"(^|\s+)-I", - subprocess.check_output(command_cflags, stderr=stderr) - .decode("utf8") - .strip(), + subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags except Exception: From 1f0573f2d61ae7e79f3e86481449765cb794e5f3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 06:38:10 +1100 Subject: [PATCH 162/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 969f4be08..f10daf004 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Removed absolute path to ldconfig #7044 + [radarhere] + - Support custom comments and PLT markers when saving JPEG2000 images #6903 [joshware, radarhere, hugovk] From b18efc775d5de4736e29b938a56325a9ab785b4e Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 01:48:17 +0200 Subject: [PATCH 163/512] do not discard ImportError message in ImageFont --- src/PIL/ImageFont.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 30f6694e6..e3f72f52e 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -54,17 +54,12 @@ def __getattr__(name): raise AttributeError(msg) -class _ImagingFtNotInstalled: - # module placeholder - def __getattr__(self, id): - msg = "The _imagingft C module is not installed" - raise ImportError(msg) - - try: from . import _imagingft as core -except ImportError: - core = _ImagingFtNotInstalled() +except ImportError as ex: + from ._util import DeferredError + + core = DeferredError(ex) _UNSPECIFIED = object() From e97167401104c75e04e7966978d0388886314dd2 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 02:08:58 +0200 Subject: [PATCH 164/512] cleanup in _imagingft --- src/_imagingft.c | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 4f44d6a71..c0082b4a7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -33,12 +33,6 @@ #include FT_COLOR_H #endif -#define KEEP_PY_UNICODE - -#if !defined(FT_LOAD_TARGET_MONO) -#define FT_LOAD_TARGET_MONO FT_LOAD_MONOCHROME -#endif - /* -------------------------------------------------------------------- */ /* error table */ @@ -420,11 +414,9 @@ text_layout_fallback( if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif for (i = 0; font_getchar(string, i, &ch); i++) { (*glyph_info)[i].index = FT_Get_Char_Index(self->face, ch); error = FT_Load_Glyph(self->face, (*glyph_info)[i].index, load_flags); @@ -581,11 +573,9 @@ font_getsize(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * text bounds are given by: @@ -844,11 +834,9 @@ font_render(FontObject *self, PyObject *args) { if (mask) { load_flags |= FT_LOAD_TARGET_MONO; } -#ifdef FT_LOAD_COLOR if (color) { load_flags |= FT_LOAD_COLOR; } -#endif /* * calculate x_min and y_max @@ -958,13 +946,11 @@ font_render(FontObject *self, PyObject *args) { /* bitmap is now FT_PIXEL_MODE_GRAY, fall through */ case FT_PIXEL_MODE_GRAY: break; -#ifdef FT_LOAD_COLOR case FT_PIXEL_MODE_BGRA: if (color) { break; } /* we didn't ask for color, fall through to default */ -#endif default: PyErr_SetString(PyExc_OSError, "unsupported bitmap pixel mode"); goto glyph_error; @@ -995,7 +981,6 @@ font_render(FontObject *self, PyObject *args) { } else { target = im->image8[yy] + xx; } -#ifdef FT_LOAD_COLOR if (color && bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { /* paste color glyph */ for (k = x0; k < x1; k++) { @@ -1010,9 +995,7 @@ font_render(FontObject *self, PyObject *args) { target[k * 4 + 3] = source[k * 4 + 3]; } } - } else -#endif - if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + } else if (bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { if (color) { unsigned char *ink = (unsigned char *)&foreground_ink; for (k = x0; k < x1; k++) { From 3d4e9b107d22a3a3c4dee6fb20eb25d6f2639da8 Mon Sep 17 00:00:00 2001 From: nulano Date: Fri, 31 Mar 2023 02:57:58 +0200 Subject: [PATCH 165/512] warn if module is found but fails to import in PIL.features --- src/PIL/features.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 6f9d99e76..80a16a75e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -33,7 +33,10 @@ def check_module(feature): try: __import__(module) return True - except ImportError: + except ModuleNotFoundError: + return False + except ImportError as ex: + warnings.warn(str(ex)) return False @@ -145,7 +148,10 @@ def check_feature(feature): try: imported_module = __import__(module, fromlist=["PIL"]) return getattr(imported_module, flag) - except ImportError: + except ModuleNotFoundError: + return None + except ImportError as ex: + warnings.warn(str(ex)) return None From e95b55acd4d600b793f069c3c1fb266ec9ebe202 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 20:48:14 +1100 Subject: [PATCH 166/512] Document loss of palette when converting to NumPy --- src/PIL/Image.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 95f5a9bc1..cc0b90b1d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3031,21 +3031,25 @@ def frombuffer(mode, size, data, decoder_name="raw", *args): def fromarray(obj, mode=None): """ Creates an image memory from an object exporting the array interface - (using the buffer protocol). + (using the buffer protocol):: + + from PIL import Image + import numpy as np + a = np.zeros((5, 5)) + im = Image.fromarray(a) If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. - If you have an image in NumPy:: + Pillow images can also be converted to arrays:: from PIL import Image import numpy as np im = Image.open("hopper.jpg") a = np.asarray(im) - Then this can be used to convert it to a Pillow image:: - - im = Image.fromarray(a) + When converting Pillow images to arrays however, only pixel values are + transferred. This means that P and PA mode images will lose their palette. :param obj: Object with array interface :param mode: Optional mode to use when reading ``obj``. Will be determined from From 485532c1f3c3cf146a37b88f81dd6a0ff077daf1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:00:28 +1100 Subject: [PATCH 167/512] Mention available pixel types when converting from NumPy --- src/PIL/Image.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index cc0b90b1d..259616e2b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3041,6 +3041,10 @@ def fromarray(obj, mode=None): If ``obj`` is not contiguous, then the ``tobytes`` method is called and :py:func:`~PIL.Image.frombuffer` is used. + In the case of NumPy, be aware that Pillow modes do not always correspond + to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, + 32-signed integer pixels and 32-bit floating point pixels. + Pillow images can also be converted to arrays:: from PIL import Image From d84e227204bf95414481a933ef3299540c066f2f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:52:37 +1100 Subject: [PATCH 168/512] Fixed warning that variable may be uninitialized --- src/_imaging.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imaging.c b/src/_imaging.c index 728adc07b..d7b90b9e8 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -502,7 +502,7 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - int tupleSize; + int tupleSize = 0; if (PyTuple_Check(color)) { tupleSize = PyTuple_GET_SIZE(color); if (tupleSize == 1) { From 59d67fa68a2e0574fd6bc9047e1dea36318b6117 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 21:59:06 +1100 Subject: [PATCH 169/512] Only call PyTuple_Check once in getink --- src/_imaging.c | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index d7b90b9e8..281f3a4d2 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -502,12 +502,9 @@ getink(PyObject *color, Imaging im, char *ink) { be cast to either UINT8 or INT32 */ int rIsInt = 0; - int tupleSize = 0; - if (PyTuple_Check(color)) { - tupleSize = PyTuple_GET_SIZE(color); - if (tupleSize == 1) { - color = PyTuple_GetItem(color, 0); - } + int tupleSize = PyTuple_Check(color) ? PyTuple_GET_SIZE(color) : -1; + if (tupleSize == 1) { + color = PyTuple_GetItem(color, 0); } if (im->type == IMAGING_TYPE_UINT8 || im->type == IMAGING_TYPE_INT32 || im->type == IMAGING_TYPE_SPECIAL) { @@ -521,7 +518,7 @@ getink(PyObject *color, Imaging im, char *ink) { PyErr_SetString( PyExc_TypeError, "color must be int or single-element tuple"); return NULL; - } else if (!PyTuple_Check(color)) { + } else if (tupleSize == -1) { PyErr_SetString(PyExc_TypeError, "color must be int or tuple"); return NULL; } From 89d2cdfcfa21f9a13499ef19d4fb9514a02691fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 31 Mar 2023 22:30:36 +1100 Subject: [PATCH 170/512] Fixed warning that nLeft is set but not used --- src/libImaging/Quant.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 783852c24..02a4a5c76 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -341,7 +341,10 @@ splitlists( PixelList *l, *r, *c, *n; int i; - int nRight, nLeft; + int nRight; +#ifndef NO_OUTPUT + int nLeft; +#endif int splitColourVal; #ifdef TEST_SPLIT @@ -396,12 +399,17 @@ splitlists( } #endif nCount[0] = nCount[1] = 0; - nLeft = nRight = 0; + nRight = 0; +#ifndef NO_OUTPUT + nLeft = 0; +#endif for (left = 0, c = h[axis]; c;) { left = left + c->count; nCount[0] += c->count; c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif c = c->next[axis]; if (left * 2 > pixelCount) { break; @@ -414,7 +422,9 @@ splitlists( break; } c->flag = 0; +#ifndef NO_OUTPUT nLeft++; +#endif nCount[0] += c->count; } } @@ -430,7 +440,9 @@ splitlists( } c->flag = 1; nRight++; +#ifndef NO_OUTPUT nLeft--; +#endif nCount[0] -= c->count; nCount[1] += c->count; } From b606da7f0eb73e2f40af3a162c7c4fc19e6a2723 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 31 Mar 2023 07:19:33 -0500 Subject: [PATCH 171/512] add missing word --- 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 259616e2b..4a142a008 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3043,7 +3043,7 @@ def fromarray(obj, mode=None): In the case of NumPy, be aware that Pillow modes do not always correspond to NumPy dtypes. Pillow modes only offer 1-bit pixels, 8-bit pixels, - 32-signed integer pixels and 32-bit floating point pixels. + 32-bit signed integer pixels, and 32-bit floating point pixels. Pillow images can also be converted to arrays:: From 5932a0bd19b513c3c86d9fc8dec5d41384247056 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 09:23:57 +1100 Subject: [PATCH 172/512] Clear half token after use --- Tests/test_file_ppm.py | 10 ++++++++++ docs/releasenotes/9.5.0.rst | 15 ++++++++++++--- src/PIL/PpmImagePlugin.py | 1 + 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fbcbea6c6..292642ca9 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -256,6 +256,16 @@ def test_truncated_file(tmp_path): im.load() +def test_not_enough_image_data(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P2 1 2 255 255") + + with Image.open(path) as im: + with pytest.raises(ValueError): + im.load() + + @pytest.mark.parametrize("maxval", (b"0", b"65536")) def test_invalid_maxval(maxval, tmp_path): path = str(tmp_path / "temp.ppm") diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 0b0e0dd2f..1ba9b9890 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -62,10 +62,19 @@ PLT markers. Security ======== -TODO -^^^^ +Clear PPM half token after use +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +Image files that are small on disk are often prevented from expanding to be +big images consuming a large amount of resources simply because they lack the +data to populate those resources. + +PpmImagePlugin might hold onto the last data read for a pixel value in case the +pixel value has not been finished yet. However, that data was not being cleared +afterwards, meaning that infinite data could be available to fill any image +size. + +That data is now cleared after use. Other Changes ============= diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 5aa418044..2cb1e5636 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -237,6 +237,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder): if half_token: block = half_token + block # stitch half_token to new block + half_token = False tokens = block.split() From aa9ecac0328a7dce7d7f3bd3d0ee7a9ec0316f83 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 10:27:39 +1100 Subject: [PATCH 173/512] Added ImageSourceData to TAGS_V2 --- Tests/test_file_libtiff.py | 5 +++++ docs/releasenotes/9.5.0.rst | 10 +++++++++- src/PIL/TiffTags.py | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 7a94c0302..53cfd81f7 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,6 +668,11 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) + def test_save_imagesourcedata(self, tmp_path): + outfile = str(tmp_path / "temp.tif") + with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + im.save(outfile) + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index 1ba9b9890..915721082 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -72,10 +72,18 @@ data to populate those resources. PpmImagePlugin might hold onto the last data read for a pixel value in case the pixel value has not been finished yet. However, that data was not being cleared afterwards, meaning that infinite data could be available to fill any image -size. +size. This has been present since Pillow 9.2.0. That data is now cleared after use. +Saving TIFF tag ImageSourceData +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If Pillow incorrectly saved the TIFF tag ImageSourceData as ASCII instead of +UNDEFINED, a segmentation fault was triggered. + +The correct tag type will now be used by default instead. + Other Changes ============= diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index ac048ba56..30b05e4e1 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -195,6 +195,7 @@ TAGS_V2 = { 34675: ("ICCProfile", UNDEFINED, 1), 34853: ("GPSInfoIFD", LONG, 1), 36864: ("ExifVersion", UNDEFINED, 1), + 37724: ("ImageSourceData", UNDEFINED, 1), 40965: ("InteroperabilityIFD", LONG, 1), 41730: ("CFAPattern", UNDEFINED, 1), # MPInfo From b1b0353d17bcdca99cfcb2ea48c6af7861fb43ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 12:21:16 +1100 Subject: [PATCH 174/512] Corrected passing TIFF_LONG to libtiff --- Tests/test_file_libtiff.py | 7 ++++++- src/encode.c | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 53cfd81f7..ac78b0869 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -668,11 +668,16 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) - def test_save_imagesourcedata(self, tmp_path): + def test_exif_ifd(self, tmp_path): outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert im.tag_v2[34665] == 125456 im.save(outfile) + with Image.open(outfile) as reloaded: + if Image.core.libtiff_support_custom_tags: + assert reloaded.tag_v2[34665] == 125456 + def test_crashing_metadata(self, tmp_path): # issue 1597 with Image.open("Tests/images/rdf.tif") as im: diff --git a/src/encode.c b/src/encode.c index a66594935..308bd2059 100644 --- a/src/encode.c +++ b/src/encode.c @@ -904,7 +904,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) { &encoder->state, (ttag_t)key_int, (UINT16)PyLong_AsLong(value)); } else if (type == TIFF_LONG) { status = ImagingLibTiffSetField( - &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)); + &encoder->state, (ttag_t)key_int, PyLong_AsLongLong(value)); } else if (type == TIFF_SSHORT) { status = ImagingLibTiffSetField( &encoder->state, (ttag_t)key_int, (INT16)PyLong_AsLong(value)); From ac7878082e4caea5fea5ac4db26425e53ffa037c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 14:06:42 +1100 Subject: [PATCH 175/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f10daf004..43978bea4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 9.5.0 (unreleased) ------------------ +- Added ImageSourceData to TAGS_V2 #7053 + [radarhere] + +- Clear PPM half token after use #7052 + [radarhere] + - Removed absolute path to ldconfig #7044 [radarhere] From 10794e0d66e253868ecede1828282247d71bc7ac Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 09:15:01 +0300 Subject: [PATCH 176/512] 9.5.0 version bump --- CHANGES.rst | 2 +- src/PIL/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 43978bea4..b77017f8a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,7 @@ Changelog (Pillow) ================== -9.5.0 (unreleased) +9.5.0 (2023-04-01) ------------------ - Added ImageSourceData to TAGS_V2 #7053 diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 7baa9fb6c..d94d35934 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0.dev0" +__version__ = "9.5.0" From fa689fba04577830eba4f4f4f923d55f9c4bc833 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 18:35:43 +1100 Subject: [PATCH 177/512] Removed unused sections --- docs/releasenotes/9.5.0.rst | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/releasenotes/9.5.0.rst b/docs/releasenotes/9.5.0.rst index bda3cd05a..b1e982fcc 100644 --- a/docs/releasenotes/9.5.0.rst +++ b/docs/releasenotes/9.5.0.rst @@ -1,14 +1,6 @@ 9.5.0 ----- -Backwards Incompatible Changes -============================== - -TODO -^^^^ - -TODO - Deprecations ============ @@ -20,14 +12,6 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -API Changes -=========== - -TODO -^^^^ - -TODO - API Additions ============= From 2203afeafa76519fcc01682d8c35e5c5d569d7c6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 19:36:06 +1100 Subject: [PATCH 178/512] Do not set size unnecessarily if image failed to open --- src/PIL/EpsImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..2f7fee901 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -354,7 +354,6 @@ class EpsImageFile(ImageFile.ImageFile): check_required_header_comments() if not self._size: - self._size = 1, 1 # errors if this isn't set. why (1,1)? msg = "cannot determine EPS bounding box" raise OSError(msg) From 4f7070e24c9cba8fc7ffab8d9f1c291977bbf183 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 12:34:27 +0300 Subject: [PATCH 179/512] 9.6.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index d94d35934..c5c37fe91 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.5.0" +__version__ = "9.6.0.dev0" From 1e250e1137171101e88cd1e1fbd2962e967b4aac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Apr 2023 23:36:52 +1100 Subject: [PATCH 180/512] 10.0.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index c5c37fe91..800203d51 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "9.6.0.dev0" +__version__ = "10.0.0.dev0" From 596569c928610a42f2f75a5c3b259d961de62ebf Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 15:58:08 +0300 Subject: [PATCH 181/512] Drop support for soon-EOL Python 3.7 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 3 +-- .pre-commit-config.yaml | 2 +- Makefile | 2 +- docs/installation.rst | 8 ++++---- docs/newer-versions.csv | 3 ++- setup.cfg | 3 +-- tox.ini | 2 +- 9 files changed, 14 insertions(+), 15 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b5913e043..9ed192e0f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,7 @@ environment: - PYTHON: C:/Python311 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - - PYTHON: C:/Python37-x64 + - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 @@ -31,7 +31,7 @@ install: - path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | - c:\python37\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ + c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\pillow\winbuild\build\build_dep_all.cmd $host.SetShouldExit(0) - path C:\pillow\winbuild\build\bin;%PATH% diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index ba72cb7b8..8f4d53ecf 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] + python-version: ["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/.github/workflows/test.yml b/.github/workflows/test.yml index 10c3cd929..fced6113b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,9 @@ jobs: "3.10", "3.9", "3.8", - "3.7", ] include: - - python-version: "3.7" + - python-version: "3.9" PYTHONOPTIMIZE: 1 REVERSE: "--reverse" - python-version: "3.8" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 45c1f3c5f..b3cda9965 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 23.1.0 hooks: - id: black - args: [--target-version=py37] + args: [--target-version=py38] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] diff --git a/Makefile b/Makefile index bb0ea60b3..f51325d47 100644 --- a/Makefile +++ b/Makefile @@ -123,5 +123,5 @@ lint: lint-fix: python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black python3 -c "import isort" > /dev/null 2>&1 || python3 -m pip install isort - python3 -m black --target-version py37 . + python3 -m black --target-version py38 . python3 -m isort . diff --git a/docs/installation.rst b/docs/installation.rst index 9ec15a8f1..4f12a5713 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -454,22 +454,22 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 12 Monterey | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| macOS 12 Monterey | 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.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.7, 3.8, 3.9, 3.10, 3.11, | x86-64 | +| Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.10 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2016 | 3.7 | x86-64 | +| Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.7, 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ | | 3.9 (MinGW) | x86, x86-64 | diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index ed2369259..d53947ff5 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,5 +1,6 @@ Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 9.3,Yes,Yes,Yes,Yes,Yes,, +Pillow >= 10,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, diff --git a/setup.cfg b/setup.cfg index d6057f159..06e95d7cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,6 @@ classifiers = License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -36,7 +35,7 @@ project_urls = [options] packages = PIL -python_requires = >=3.7 +python_requires = >=3.8 include_package_data = True package_dir = = src diff --git a/tox.ini b/tox.ini index 9a41ca96b..95a6a4563 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = lint - py{py3, 311, 310, 39, 38, 37} + py{py3, 311, 310, 39, 38} minversion = 1.9 [testenv] From b399ebc8c2fbe5e9be5883fac2cd7f3918d98100 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 1 Apr 2023 17:02:39 +0300 Subject: [PATCH 182/512] Bump Python on RTD from 3.7 to 3.11 --- .readthedocs.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0f581ebba..98d9e4425 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,10 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: install: - method: pip From 6a2087ebe40ca70800143f8d3a9793a0267b5b3e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 17:46:00 +0300 Subject: [PATCH 183/512] Amazon Linux 2 now tested with Python 3.9 --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 4f12a5713..7088657f9 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -434,7 +434,7 @@ These platforms are built and tested for every change. +==================================+============================+=====================+ | Alpine | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | +| Amazon Linux 2 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Amazon Linux 2023 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 00d18fc100011613f8f1c114293b2326065790f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Apr 2023 07:39:31 +1000 Subject: [PATCH 184/512] Updated Python version in documentation --- winbuild/README.md | 2 +- winbuild/build.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 21b40d4e6..2975acf28 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -18,7 +18,7 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python37\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +C:\Python39\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. diff --git a/winbuild/build.rst b/winbuild/build.rst index e83045f0c..99dfad301 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -112,7 +112,7 @@ The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python37\bin\python.exe build_prepare.py -v --depends C:\pillow-depends + C:\Python39\bin\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd build\build_pillow.cmd install cd .. From fada40d2e8f613cb040aa7465d40c17db8e37069 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 3 Apr 2023 19:29:09 +1000 Subject: [PATCH 185/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b77017f8a..547893d82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.0.0 (unreleased) +------------------- + +- Drop support for soon-EOL Python 3.7 #7058 + [hugovk, radarhere] + 9.5.0 (2023-04-01) ------------------ From 5a7e2ad638d1b3cb8d34006d47154d35b2df9c74 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Apr 2023 19:44:19 +0000 Subject: [PATCH 186/512] [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: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0) - [github.com/PyCQA/bandit: 1.7.4 → 1.7.5](https://github.com/PyCQA/bandit/compare/1.7.4...1.7.5) - [github.com/Lucas-C/pre-commit-hooks: v1.4.2 → v1.5.1](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.4.2...v1.5.1) - [github.com/tox-dev/tox-ini-fmt: 0.6.1 → 1.0.0](https://github.com/tox-dev/tox-ini-fmt/compare/0.6.1...1.0.0) --- .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 b3cda9965..51c7117d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black args: [--target-version=py38] @@ -14,7 +14,7 @@ repos: - id: isort - repo: https://github.com/PyCQA/bandit - rev: 1.7.4 + rev: 1.7.5 hooks: - id: bandit args: [--severity-level=high] @@ -26,7 +26,7 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.4.2 + rev: v1.5.1 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 0.6.1 + rev: 1.0.0 hooks: - id: tox-ini-fmt From f46594ff47f3401d535588ee9b3f2259d3c387d3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 3 Apr 2023 23:07:45 +0300 Subject: [PATCH 187/512] Don't remove tabs from hopper.gd --- .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 51c7117d7..c3b6dc0a6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: rev: v1.5.1 hooks: - id: remove-tabs - exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.opt$) + exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 From 6c057b32771cd24e76656ca5b92a107e43eb08f8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 3 Apr 2023 23:08:23 +0300 Subject: [PATCH 188/512] Run tox-ini-fmt --- tox.ini | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 95a6a4563..d7948ef6d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] +minversion = 1.9 envlist = lint py{py3, 311, 310, 39, 38} -minversion = 1.9 [testenv] deps = @@ -15,15 +15,16 @@ commands = {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . {envpython} selftest.py {envpython} -m pytest -W always {posargs} -allowlist_externals = make +allowlist_externals = + make [testenv:lint] -passenv = - PRE_COMMIT_COLOR skip_install = true deps = check-manifest pre-commit +passenv = + PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure check-manifest From 498b475e82105fcad0b78465dd259973849fbd55 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Apr 2023 13:23:19 +1000 Subject: [PATCH 189/512] Added links to more information about the enhancement factor --- docs/reference/ImageEnhance.rst | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index b27228ec9..746f18cd2 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -29,6 +29,8 @@ Classes All enhancement classes implement a common interface, containing a single method: +.. _enhancement-factor: + .. py:class:: _Enhance .. py:method:: enhance(factor) @@ -45,31 +47,33 @@ method: Adjust image color balance. - This class can be used to adjust the colour balance of an image, in - a manner similar to the controls on a colour TV set. An enhancement - factor of 0.0 gives a black and white image. A factor of 1.0 gives - the original image. + This class can be used to adjust the colour balance of an image, in a + manner similar to the controls on a colour TV set. An + :ref:`enhancement factor ` of 0.0 gives a black and + white image. A factor of 1.0 gives the original image. .. py:class:: Contrast(image) Adjust image contrast. - This class can be used to control the contrast of an image, similar - to the contrast control on a TV set. An enhancement factor of 0.0 - gives a solid grey image. A factor of 1.0 gives the original image. + This class can be used to control the contrast of an image, similar to the + contrast control on a TV set. An + :ref:`enhancement factor ` of 0.0 gives a solid grey + image. A factor of 1.0 gives the original image. .. py:class:: Brightness(image) Adjust image brightness. - This class can be used to control the brightness of an image. An - enhancement factor of 0.0 gives a black image. A factor of 1.0 gives the - original image. + This class can be used to control the brightness of an image. An + :ref:`enhancement factor ` of 0.0 gives a black image. + A factor of 1.0 gives the original image. .. py:class:: Sharpness(image) Adjust image sharpness. This class can be used to adjust the sharpness of an image. An - enhancement factor of 0.0 gives a blurred image, a factor of 1.0 gives the - original image, and a factor of 2.0 gives a sharpened image. + :ref:`enhancement factor ` of 0.0 gives a blurred + image, a factor of 1.0 gives the original image, and a factor of 2.0 gives + a sharpened image. From 4720774a8523e69bbca5b9fb71c1f38c0ba38151 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Apr 2023 14:59:26 +1000 Subject: [PATCH 190/512] Describe the effect of brightness and contrast factors above 1 --- docs/reference/ImageEnhance.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/reference/ImageEnhance.rst b/docs/reference/ImageEnhance.rst index 746f18cd2..457f0d4df 100644 --- a/docs/reference/ImageEnhance.rst +++ b/docs/reference/ImageEnhance.rst @@ -59,15 +59,17 @@ method: This class can be used to control the contrast of an image, similar to the contrast control on a TV set. An :ref:`enhancement factor ` of 0.0 gives a solid grey - image. A factor of 1.0 gives the original image. + image, a factor of 1.0 gives the original image, and greater values + increase the contrast of the image. .. py:class:: Brightness(image) Adjust image brightness. This class can be used to control the brightness of an image. An - :ref:`enhancement factor ` of 0.0 gives a black image. - A factor of 1.0 gives the original image. + :ref:`enhancement factor ` of 0.0 gives a black image, + a factor of 1.0 gives the original image, and greater values increase the + brightness of the image. .. py:class:: Sharpness(image) From ca2bf046d35ec41251716cac152fc9209964a359 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 09:57:16 +1000 Subject: [PATCH 191/512] Use "/sbin/ldconfig" if ldconfig is not found --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d6..f9670d4c0 100755 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ import os import re +import shutil import struct import subprocess import sys @@ -150,6 +151,7 @@ def _dbg(s, tp=None): def _find_library_dirs_ldconfig(): # Based on ctypes.util from Python 2 + ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): if struct.calcsize("l") == 4: machine = os.uname()[4] + "-32" @@ -166,14 +168,14 @@ def _find_library_dirs_ldconfig(): # Assuming GLIBC's ldconfig (with option -p) # Alpine Linux uses musl that can't print cache - args = ["ldconfig", "-p"] + args = [ldconfig, "-p"] expr = rf".*\({abi_type}.*\) => (.*)" env = dict(os.environ) env["LC_ALL"] = "C" env["LANG"] = "C" elif sys.platform.startswith("freebsd"): - args = ["ldconfig", "-r"] + args = [ldconfig, "-r"] expr = r".* => (.*)" env = {} From d94239ae3d21d8ae03f5120228dc8225faa99bac Mon Sep 17 00:00:00 2001 From: adisbladis Date: Wed, 5 Apr 2023 15:26:46 +1200 Subject: [PATCH 192/512] Handle polymorphic types for lib_root and include_root in setup.py Depending on whether these are created by pkg_config or not they might be a list of directories or just a string with a single directory. --- setup.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d6..d780b038a 100755 --- a/setup.py +++ b/setup.py @@ -473,11 +473,17 @@ class pil_build_ext(build_ext): lib_root = include_root = root if lib_root is not None: - for lib_dir in lib_root: - _add_directory(library_dirs, lib_dir) + if isinstance(lib_root, str): + _add_directory(library_dirs, lib_root) + else: + 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) + if isinstance(include_root, str): + _add_directory(include_dirs, include_root) + else: + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From b2b660e2c0b6f209916a2bd88b22f650a86fabce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Apr 2023 18:53:28 +1000 Subject: [PATCH 193/512] Removed FIXME comment --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 07d6c66d6..589a1471e 100755 --- a/setup.py +++ b/setup.py @@ -681,10 +681,6 @@ class pil_build_ext(build_ext): # Add the directory to the include path so we can include # rather than having to cope with the versioned # include path - # FIXME (melvyn-sopacua): - # At this point it's possible that best_path is already in - # self.compiler.include_dirs. Should investigate how that is - # possible. _add_directory(self.compiler.include_dirs, best_path, 0) feature.jpeg2000 = "openjp2" feature.openjpeg_version = ".".join(str(x) for x in best_version) From 59c9d87f8a4d8443a482ca7abb5f1f8d5d970cbd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 00:27:22 +0300 Subject: [PATCH 194/512] Remove support for PyQt5 and PySide2, deprecated in 9.2.0 --- .github/workflows/test-cygwin.yml | 2 +- Tests/test_deprecated_imageqt.py | 18 ------------------ Tests/test_imageqt.py | 11 ++--------- Tests/test_qt_image_qapplication.py | 14 +------------- Tests/test_qt_image_toqimage.py | 6 +----- docs/deprecations.rst | 26 +++++++++++++------------- docs/reference/ImageQt.rst | 14 +++----------- src/PIL/ImageQt.py | 13 ------------- 8 files changed, 21 insertions(+), 83 deletions(-) delete mode 100644 Tests/test_deprecated_imageqt.py diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 6c9ed66e3..397cfe0a1 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -67,7 +67,7 @@ jobs: python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt5-devel-tools + qt6-devel-tools wget xorg-server-extra zlib-devel diff --git a/Tests/test_deprecated_imageqt.py b/Tests/test_deprecated_imageqt.py deleted file mode 100644 index 2528ff3f7..000000000 --- a/Tests/test_deprecated_imageqt.py +++ /dev/null @@ -1,18 +0,0 @@ -import warnings - -with warnings.catch_warnings(record=True) as w: - # Arrange: cause all warnings to always be triggered - warnings.simplefilter("always") - - # Act: trigger a warning with Qt5 - from PIL import ImageQt - - -def test_deprecated(): - # Assert - if ImageQt.qt_version in ("5", "side2"): - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "deprecated" in str(w[0].message) - else: - assert len(w) == 0 diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2f2b07918..2c73a2094 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -2,13 +2,10 @@ import warnings import pytest +from PIL import ImageQt + from .helper import assert_image_similar, hopper -with warnings.catch_warnings() as w: - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt - - pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -26,10 +23,6 @@ def test_rgb(): from PyQt6.QtGui import qRgb elif ImageQt.qt_version == "side6": from PySide6.QtGui import qRgb - elif ImageQt.qt_version == "5": - from PyQt5.QtGui import qRgb - elif ImageQt.qt_version == "side2": - from PySide2.QtGui import qRgb assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 4929fa933..5d2e41212 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper @@ -19,14 +15,6 @@ if ImageQt.qt_is_installed: from PySide6.QtCore import QPoint from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "5": - from PyQt5.QtCore import QPoint - from PyQt5.QtGui import QImage, QPainter, QRegion - from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - elif ImageQt.qt_version == "side2": - from PySide2.QtCore import QPoint - from PySide2.QtGui import QImage, QPainter, QRegion - from PySide2.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget class Example(QWidget): def __init__(self): diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index c1983031a..399df670f 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,10 +1,6 @@ -import warnings - import pytest -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - from PIL import ImageQt +from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 5669d2827..48e8b6d93 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -135,19 +135,6 @@ PhotoImage.paste box parameter The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). -PyQt5 and PySide2 -~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. - Image.coerce_e ~~~~~~~~~~~~~~ @@ -223,6 +210,19 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +PyQt5 and PySide2 +~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/reference/ImageQt.rst b/docs/reference/ImageQt.rst index 15d052d1c..7e67a44d3 100644 --- a/docs/reference/ImageQt.rst +++ b/docs/reference/ImageQt.rst @@ -4,16 +4,8 @@ :py:mod:`~PIL.ImageQt` Module ============================= -The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6, PySide6, PyQt5 -or PySide2 QImage objects from PIL images. - -`Qt 5 reached end-of-life `_ on 2020-12-08 for -open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). - -Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed -in Pillow 10 (2023-07-01). Upgrade to -`PyQt6 `_ or -`PySide6 `_ instead. +The :py:mod:`~PIL.ImageQt` module contains support for creating PyQt6 or PySide6 +QImage objects from PIL images. .. versionadded:: 1.1.6 @@ -22,7 +14,7 @@ in Pillow 10 (2023-07-01). Upgrade to Creates an :py:class:`~PIL.ImageQt.ImageQt` object from a PIL :py:class:`~PIL.Image.Image` object. This class is a subclass of QtGui.QImage, which means that you can pass the resulting objects directly - to PyQt6/PySide6/PyQt5/PySide2 API functions and methods. + to PyQt6/PySide6 API functions and methods. This operation is currently supported for mode 1, L, P, RGB, and RGBA images. To handle other modes, you need to convert the image first. diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index ad607a97b..9b7245454 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -20,14 +20,11 @@ import sys from io import BytesIO from . import Image -from ._deprecate import deprecate from ._util import is_path qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], - ["5", "PyQt5"], - ["side2", "PySide2"], ] # If a version has already been imported, attempt it first @@ -40,16 +37,6 @@ for qt_version, qt_module in qt_versions: elif qt_module == "PySide6": from PySide6.QtCore import QBuffer, QIODevice from PySide6.QtGui import QImage, QPixmap, qRgba - elif qt_module == "PyQt5": - from PyQt5.QtCore import QBuffer, QIODevice - from PyQt5.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PyQt5", 10, "PyQt6 or PySide6") - elif qt_module == "PySide2": - from PySide2.QtCore import QBuffer, QIODevice - from PySide2.QtGui import QImage, QPixmap, qRgba - - deprecate("Support for PySide2", 10, "PyQt6 or PySide6") except (ImportError, RuntimeError): continue qt_is_installed = True From 070e7704695f92ea829c678b58bb1ce5b8cbc51a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 19:32:26 +0300 Subject: [PATCH 195/512] Remove support for Tk/Tcl <= 8.4, deprecated in 8.2.0 --- docs/deprecations.rst | 16 +++++------ src/PIL/_tkinter_finder.py | 6 ---- src/Tk/_tkmini.h | 14 +-------- src/Tk/tkImaging.c | 58 +++++++++----------------------------- 4 files changed, 22 insertions(+), 72 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 48e8b6d93..47d1531ae 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Tk/Tcl 8.4 -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -Support for Tk/Tcl 8.4 is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -when Tk/Tcl 8.5 will be the minimum supported. - Categories ~~~~~~~~~~ @@ -210,6 +202,14 @@ Removed features Deprecated features are only removed in major releases after an appropriate period of deprecation has passed. +Tk/Tcl 8.4 +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 5cd7e9b1f..597c21b5e 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -4,8 +4,6 @@ import sys import tkinter from tkinter import _tkinter as tk -from ._deprecate import deprecate - try: if hasattr(sys, "pypy_find_executable"): TKINTER_LIB = tk.tklib_cffi.__file__ @@ -17,7 +15,3 @@ except AttributeError: TKINTER_LIB = None tk_version = str(tkinter.TkVersion) -if tk_version == "8.4": - deprecate( - "Support for Tk/Tcl 8.4", 10, action="Please upgrade to Tk/Tcl 8.5 or newer" - ) diff --git a/src/Tk/_tkmini.h b/src/Tk/_tkmini.h index 9852fc9d6..68247bc47 100644 --- a/src/Tk/_tkmini.h +++ b/src/Tk/_tkmini.h @@ -119,17 +119,7 @@ typedef struct Tk_PhotoImageBlock { } Tk_PhotoImageBlock; /* Typedefs derived from function signatures in Tk header */ -/* Tk_PhotoPutBlock for Tk <= 8.4 */ -typedef void (*Tk_PhotoPutBlock_84_t)( - Tk_PhotoHandle handle, - Tk_PhotoImageBlock *blockPtr, - int x, - int y, - int width, - int height, - int compRule); -/* Tk_PhotoPutBlock for Tk >= 8.5 */ -typedef int (*Tk_PhotoPutBlock_85_t)( +typedef int (*Tk_PhotoPutBlock_t)( Tcl_Interp *interp, Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr, @@ -138,8 +128,6 @@ typedef int (*Tk_PhotoPutBlock_85_t)( int width, int height, int compRule); -/* Tk_PhotoSetSize for Tk <= 8.4 */ -typedef void (*Tk_PhotoSetSize_84_t)(Tk_PhotoHandle handle, int width, int height); /* Tk_FindPhoto */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t)(Tcl_Interp *interp, const char *imageName); /* Tk_PhotoGetImage */ diff --git a/src/Tk/tkImaging.c b/src/Tk/tkImaging.c index ad503baec..bd3cafe95 100644 --- a/src/Tk/tkImaging.c +++ b/src/Tk/tkImaging.c @@ -48,14 +48,11 @@ * Global vars for Tcl / Tk functions. We load these symbols from the tkinter * extension module or loaded Tcl / Tk libraries at run-time. */ -static int TK_LT_85 = 0; static Tcl_CreateCommand_t TCL_CREATE_COMMAND; static Tcl_AppendResult_t TCL_APPEND_RESULT; static Tk_FindPhoto_t TK_FIND_PHOTO; static Tk_PhotoGetImage_t TK_PHOTO_GET_IMAGE; -static Tk_PhotoPutBlock_84_t TK_PHOTO_PUT_BLOCK_84; -static Tk_PhotoSetSize_84_t TK_PHOTO_SET_SIZE_84; -static Tk_PhotoPutBlock_85_t TK_PHOTO_PUT_BLOCK_85; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; static Imaging ImagingFind(const char *name) { @@ -130,26 +127,15 @@ PyImagingPhotoPut( block.pitch = im->linesize; block.pixelPtr = (unsigned char *)im->block; - if (TK_LT_85) { /* Tk 8.4 */ - TK_PHOTO_PUT_BLOCK_84( - photo, &block, 0, 0, block.width, block.height, TK_PHOTO_COMPOSITE_SET); - if (strcmp(im->mode, "RGBA") == 0) { - /* Tk workaround: we need apply ToggleComplexAlphaIfNeeded */ - /* (fixed in Tk 8.5a3) */ - TK_PHOTO_SET_SIZE_84(photo, block.width, block.height); - } - } else { - /* Tk >=8.5 */ - TK_PHOTO_PUT_BLOCK_85( - interp, - photo, - &block, - 0, - 0, - block.width, - block.height, - TK_PHOTO_COMPOSITE_SET); - } + TK_PHOTO_PUT_BLOCK( + interp, + photo, + &block, + 0, + 0, + block.width, + block.height, + TK_PHOTO_COMPOSITE_SET); return TCL_OK; } @@ -290,16 +276,7 @@ get_tk(HMODULE hMod) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(hMod, "Tk_FindPhoto")) == NULL) { return -1; }; - TK_LT_85 = GetProcAddress(hMod, "Tk_PhotoPutBlock_Panic") == NULL; - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - if (TK_LT_85) { - TK_PHOTO_PUT_BLOCK_84 = (Tk_PhotoPutBlock_84_t)func; - return ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(hMod, "Tk_PhotoSetSize")) == NULL) - ? -1 - : 1; - } - TK_PHOTO_PUT_BLOCK_85 = (Tk_PhotoPutBlock_85_t)func; + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)func; return 1; } @@ -422,18 +399,9 @@ _func_loader(void *lib) { if ((TK_FIND_PHOTO = (Tk_FindPhoto_t)_dfunc(lib, "Tk_FindPhoto")) == NULL) { return 1; } - /* Tk_PhotoPutBlock_Panic defined as of 8.5.0 */ - TK_LT_85 = (dlsym(lib, "Tk_PhotoPutBlock_Panic") == NULL); - if (TK_LT_85) { - return ( - ((TK_PHOTO_PUT_BLOCK_84 = - (Tk_PhotoPutBlock_84_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL) || - ((TK_PHOTO_SET_SIZE_84 = - (Tk_PhotoSetSize_84_t)_dfunc(lib, "Tk_PhotoSetSize")) == NULL)); - } return ( - (TK_PHOTO_PUT_BLOCK_85 = - (Tk_PhotoPutBlock_85_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); + (TK_PHOTO_PUT_BLOCK = + (Tk_PhotoPutBlock_t)_dfunc(lib, "Tk_PhotoPutBlock")) == NULL); } int From ddc4e902352e1d4459592707e78081c48ba4803a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 19:51:16 +0300 Subject: [PATCH 196/512] Remove im.category and related Image.NORMAL, Image.SEQUENCE, Image.CONTAINER, deprecated in 8.2.0 --- Tests/test_image.py | 11 ----------- docs/deprecations.rst | 24 ++++++++++++------------ src/PIL/Image.py | 10 ---------- 3 files changed, 12 insertions(+), 33 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 17f1edb00..cb31565e7 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -929,17 +929,6 @@ class TestImage: im.apply_transparency() assert im.palette.colors[(27, 35, 6, 214)] == 24 - def test_categories_deprecation(self): - with pytest.warns(DeprecationWarning): - assert hopper().category == 0 - - with pytest.warns(DeprecationWarning): - assert Image.NORMAL == 0 - with pytest.warns(DeprecationWarning): - assert Image.SEQUENCE == 1 - with pytest.warns(DeprecationWarning): - assert Image.CONTAINER == 2 - def test_constants(self): with pytest.warns(DeprecationWarning): assert Image.LINEAR == Image.Resampling.BILINEAR diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 47d1531ae..1e448ad31 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,18 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Categories -~~~~~~~~~~ - -.. deprecated:: 8.2.0 - -``im.category`` is deprecated and will be removed in Pillow 10.0.0 (2023-07-01), -along with the related ``Image.NORMAL``, ``Image.SEQUENCE`` and -``Image.CONTAINER`` attributes. - -To determine if an image has multiple frames or not, -``getattr(im, "is_animated", False)`` can be used instead. - JpegImagePlugin.convert_dict_qtables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -210,6 +198,18 @@ Tk/Tcl 8.4 Support for Tk/Tcl 8.4 was removed in Pillow 10.0.0 (2023-07-01). +Categories +~~~~~~~~~~ + +.. deprecated:: 8.2.0 +.. versionremoved:: 10.0.0 + +``im.category`` was removed along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4a142a008..99a895fb0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -61,10 +61,6 @@ from ._util import DeferredError, is_path def __getattr__(name): - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - deprecate("Image categories", 10, "is_animated", plural=True) - return categories[name] old_resampling = { "LINEAR": "BILINEAR", "CUBIC": "BICUBIC", @@ -521,12 +517,6 @@ class Image: self.pyaccess = None self._exif = None - def __getattr__(self, name): - if name == "category": - deprecate("Image categories", 10, "is_animated", plural=True) - return self._category - raise AttributeError(name) - @property def width(self): return self.size[0] From 52f4fc59a23fb57159d0622226461bcf1be1058a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:27:30 +0300 Subject: [PATCH 197/512] Remove JpegImagePlugin.convert_dict_qtables, deprecated in 8.3.0 --- Tests/test_file_jpeg.py | 6 ------ docs/deprecations.rst | 19 +++++++++---------- docs/releasenotes/8.3.0.rst | 2 +- src/PIL/JpegImagePlugin.py | 6 ------ 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 4981e15af..73a00386f 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -636,12 +636,6 @@ class TestFileJpeg: assert max(im2.quantization[0]) <= 255 assert max(im2.quantization[1]) <= 255 - def test_convert_dict_qtables_deprecation(self): - with pytest.warns(DeprecationWarning): - qtable = {0: [1, 2, 3, 4]} - qtable2 = JpegImagePlugin.convert_dict_qtables(qtable) - assert qtable == qtable2 - @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self): with Image.open(TEST_FILE) as img: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 1e448ad31..f70a91834 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,16 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -JpegImagePlugin.convert_dict_qtables -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.3.0 - -JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer -performs any operations on the data given to it, has been deprecated and will be -removed in Pillow 10.0.0 (2023-07-01). - ImagePalette size parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -210,6 +200,15 @@ Categories To determine if an image has multiple frames or not, ``getattr(im, "is_animated", False)`` can be used instead. +JpegImagePlugin.convert_dict_qtables +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.3.0 +.. versionremoved:: 10.0.0 + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/8.3.0.rst b/docs/releasenotes/8.3.0.rst index b9642576f..e74880f6f 100644 --- a/docs/releasenotes/8.3.0.rst +++ b/docs/releasenotes/8.3.0.rst @@ -8,7 +8,7 @@ JpegImagePlugin.convert_dict_qtables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ JPEG ``quantization`` is now automatically converted, but still returned as a -dictionary. The :py:attr:`~PIL.JpegImagePlugin.convert_dict_qtables` method no longer +dictionary. The ``convert_dict_qtables`` method no longer performs any operations on the data given to it, has been deprecated and will be removed in Pillow 10.0.0 (2023-07-01). diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 71ae84c04..5dd1a61af 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -46,7 +46,6 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 -from ._deprecate import deprecate from .JpegPresets import presets # @@ -612,11 +611,6 @@ samplings = { # fmt: on -def convert_dict_qtables(qtables): - deprecate("convert_dict_qtables", 10, action="Conversion is no longer needed") - return qtables - - def get_sampling(im): # There's no subsampling when images have only 1 layer # (grayscale images) or when they are CMYK (4 layers), From 5dbef9e0a8483e1cc680779c49d4e61275920066 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:45:51 +0300 Subject: [PATCH 198/512] Remove ImagePalette size parameter, deprecated in 8.4.0 --- Tests/test_imagepalette.py | 4 ---- docs/deprecations.rst | 21 ++++++++++----------- src/PIL/ImagePalette.py | 8 +------- 3 files changed, 11 insertions(+), 22 deletions(-) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index ac99ef381..baa698bb4 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -9,10 +9,6 @@ def test_sanity(): palette = ImagePalette.ImagePalette("RGB", list(range(256)) * 3) assert len(palette.colors) == 256 - with pytest.warns(DeprecationWarning): - with pytest.raises(ValueError): - ImagePalette.ImagePalette("RGB", list(range(256)) * 3, 10) - def test_reload(): with Image.open("Tests/images/hopper.gif") as im: diff --git a/docs/deprecations.rst b/docs/deprecations.rst index f70a91834..e740238b6 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,17 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -ImagePalette size parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 8.4.0 - -The ``size`` parameter will be removed in Pillow 10.0.0 (2023-07-01). - -Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by -default, and the size parameter could be used to override that. Pillow 8.3.0 removed -the default required length, also removing the need for the size parameter. - ImageShow.Viewer.show_file file argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -209,6 +198,16 @@ JpegImagePlugin.convert_dict_qtables Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer performed any operations on the data given to it, and has been removed. +ImagePalette size parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 8.4.0 +.. versionremoved:: 10.0.0 + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index e455c0459..f0c094708 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -19,7 +19,6 @@ import array from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile -from ._deprecate import deprecate class ImagePalette: @@ -34,16 +33,11 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode="RGB", palette=None, size=0): + def __init__(self, mode="RGB", palette=None): self.mode = mode self.rawmode = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty = None - if size != 0: - deprecate("The size parameter", 10, None) - if size != len(self.palette): - msg = "wrong palette size" - raise ValueError(msg) @property def palette(self): From 8d83d5e66a7c73dd01d7dc526afd85f5696026c5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 20:54:59 +0300 Subject: [PATCH 199/512] Remove ImageShow.Viewer.show_file file argument, deprecated in 9.1.0 --- Tests/test_imageshow.py | 17 -------- docs/deprecations.rst | 24 +++++------ src/PIL/ImageShow.py | 89 ++++------------------------------------- 3 files changed, 18 insertions(+), 112 deletions(-) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index eda485cf6..e54372b60 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -89,20 +89,3 @@ def test_ipythonviewer(): im = hopper() assert test_viewer.show(im) == 1 - - -@pytest.mark.skipif( - not on_ci() or is_win32(), - reason="Only run on CIs; hangs on Windows CIs", -) -@pytest.mark.parametrize("viewer", ImageShow._viewers) -def test_file_deprecated(tmp_path, viewer): - f = str(tmp_path / "temp.jpg") - hopper().save(f) - with pytest.warns(DeprecationWarning): - try: - viewer.show_file(file=f) - except NotImplementedError: - pass - with pytest.raises(TypeError): - viewer.show_file() diff --git a/docs/deprecations.rst b/docs/deprecations.rst index e740238b6..e75d1f7fa 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,19 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -ImageShow.Viewer.show_file file argument -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 - -The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been -deprecated and will be removed in Pillow 10.0.0 (2023-07-01). It has been replaced by -``path``. - -In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. -``viewer.show_file(file="test.jpg")`` will raise a deprecation warning, and suggest -``viewer.show_file(path="test.jpg")`` instead. - Constants ~~~~~~~~~ @@ -208,6 +195,17 @@ Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular length default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 removed the default required length, also removing the need for the ``size`` parameter. +ImageShow.Viewer.show_file file argument +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index f0e73fb90..3f68a2696 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -19,8 +19,6 @@ from shlex import quote from PIL import Image -from ._deprecate import deprecate - _viewers = [] @@ -111,21 +109,10 @@ class Viewer: """Display the given image.""" return self.show_file(self.save_image(image), **options) - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) os.system(self.get_command(path, **options)) # nosec return 1 @@ -164,21 +151,10 @@ class MacViewer(Viewer): command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" return command - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.call(["open", "-a", "Preview.app", path]) executable = sys.executable or shutil.which("python3") if executable: @@ -215,21 +191,10 @@ class XDGViewer(UnixViewer): command = executable = "xdg-open" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and will be removed in Pillow 10.0.0 (2023-07-01). ``path`` should be used - instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["xdg-open", path]) return 1 @@ -246,20 +211,10 @@ class DisplayViewer(UnixViewer): command += f" -title {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["display"] title = options.get("title") if title: @@ -278,20 +233,10 @@ class GmDisplayViewer(UnixViewer): command = "gm display" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["gm", "display", path]) return 1 @@ -304,20 +249,10 @@ class EogViewer(UnixViewer): command = "eog -n" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) subprocess.Popen(["eog", "-n", path]) return 1 @@ -336,20 +271,10 @@ class XVViewer(UnixViewer): command += f" -name {quote(title)}" return command, executable - def show_file(self, path=None, **options): + def show_file(self, path, **options): """ Display given file. - - Before Pillow 9.1.0, the first argument was ``file``. This is now deprecated, - and ``path`` should be used instead. """ - if path is None: - if "file" in options: - deprecate("The 'file' argument", 10, "'path'") - path = options.pop("file") - else: - msg = "Missing required argument: 'path'" - raise TypeError(msg) args = ["xv"] title = options.get("title") if title: From c8ec15980b00261d8c6a105a3dbc2a2fc634940c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:19:11 +0300 Subject: [PATCH 200/512] Remove constants deprecated in 9.1.0 --- Tests/test_file_apng.py | 10 ----- Tests/test_file_blp.py | 13 +----- Tests/test_file_ftex.py | 9 ---- Tests/test_image.py | 7 --- Tests/test_imagecms.py | 10 ----- Tests/test_imagefont.py | 9 ---- docs/deprecations.rst | 91 +++++++++++++++++++------------------- src/PIL/BlpImagePlugin.py | 16 ------- src/PIL/FtexImagePlugin.py | 12 ----- src/PIL/Image.py | 16 ------- src/PIL/ImageCms.py | 13 ------ src/PIL/ImageFont.py | 11 ----- src/PIL/PngImagePlugin.py | 12 ----- 13 files changed, 47 insertions(+), 182 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index feca72aa6..f78c086eb 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -655,13 +655,3 @@ def test_different_modes_in_later_frames(mode, tmp_path): im.save(test_file, save_all=True, append_images=[Image.new(mode, (1, 1))]) with Image.open(test_file) as reloaded: assert reloaded.mode == mode - - -def test_constants_deprecation(): - for enum, prefix in { - PngImagePlugin.Disposal: "APNG_DISPOSE_", - PngImagePlugin.Blend: "APNG_BLEND_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(PngImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py index ba2781820..8b1355b62 100644 --- a/Tests/test_file_blp.py +++ b/Tests/test_file_blp.py @@ -1,6 +1,6 @@ import pytest -from PIL import BlpImagePlugin, Image +from PIL import Image from .helper import ( assert_image_equal, @@ -72,14 +72,3 @@ def test_crashes(test_file): with Image.open(f) as im: with pytest.raises(OSError): im.load() - - -def test_constants_deprecation(): - for enum, prefix in { - BlpImagePlugin.Format: "BLP_FORMAT_", - BlpImagePlugin.Encoding: "BLP_ENCODING_", - BlpImagePlugin.AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(BlpImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_file_ftex.py b/Tests/test_file_ftex.py index cae20fa46..ac6253db0 100644 --- a/Tests/test_file_ftex.py +++ b/Tests/test_file_ftex.py @@ -21,12 +21,3 @@ def test_invalid_file(): with pytest.raises(SyntaxError): FtexImagePlugin.FtexImageFile(invalid_file) - - -def test_constants_deprecation(): - for enum, prefix in { - FtexImagePlugin.Format: "FORMAT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(FtexImagePlugin, prefix + name) == enum[name] diff --git a/Tests/test_image.py b/Tests/test_image.py index cb31565e7..85f9f7d02 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -930,13 +930,6 @@ class TestImage: assert im.palette.colors[(27, 35, 6, 214)] == 24 def test_constants(self): - with pytest.warns(DeprecationWarning): - assert Image.LINEAR == Image.Resampling.BILINEAR - with pytest.warns(DeprecationWarning): - assert Image.CUBIC == Image.Resampling.BICUBIC - with pytest.warns(DeprecationWarning): - assert Image.ANTIALIAS == Image.Resampling.LANCZOS - for enum in ( Image.Transpose, Image.Transform, diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 66be02078..8efe063c1 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -617,16 +617,6 @@ def test_auxiliary_channels_isolated(): assert_image_equal(test_image.convert(dst_format[2]), reference_image) -def test_constants_deprecation(): - for enum, prefix in { - ImageCms.Intent: "INTENT_", - ImageCms.Direction: "DIRECTION_", - }.items(): - 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)) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index b115517ac..2d83b5a37 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1130,12 +1130,3 @@ def test_raqm_missing_warning(monkeypatch): "Raqm layout was requested, but Raqm is not available. " "Falling back to basic layout." ) - - -def test_constants_deprecation(): - for enum, prefix in { - ImageFont.Layout: "LAYOUT_", - }.items(): - for name in enum.__members__: - with pytest.warns(DeprecationWarning): - assert getattr(ImageFont, prefix + name) == enum[name] diff --git a/docs/deprecations.rst b/docs/deprecations.rst index e75d1f7fa..8606ede4d 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,51 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Constants -~~~~~~~~~ - -.. deprecated:: 9.1.0 - -A number of constants have been deprecated and will be removed in Pillow 10.0.0 -(2023-07-01). Instead, ``enum.IntEnum`` classes have been added. - -.. note:: - - Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that - was reversed in Pillow 9.4.0 and those constants will now remain available. - See :ref:`restored-image-constants` - -===================================================== ============================================================ -Deprecated Use instead -===================================================== ============================================================ -``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` -``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` -``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` -``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` -``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` -``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` -``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` -``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` -``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` -``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` -``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` -``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` -``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` -``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` -``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` -``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` -``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` -``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` -``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` -``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` -``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` -``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` -``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` -===================================================== ============================================================ - FitsStubImagePlugin ~~~~~~~~~~~~~~~~~~~ @@ -206,6 +161,52 @@ removed and replaced by ``path``. In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. +Constants +~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +.. note:: + + Additional ``Image`` constants were deprecated in Pillow 9.1.0, but that + was reversed in Pillow 9.4.0 and those constants will now remain available. + See :ref:`restored-image-constants` + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 1cc0d4b3c..0ca60ff24 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -35,7 +35,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate class Format(IntEnum): @@ -54,21 +53,6 @@ class AlphaEncoding(IntEnum): DXT5 = 7 -def __getattr__(name): - for enum, prefix in { - Format: "BLP_FORMAT_", - Encoding: "BLP_ENCODING_", - AlphaEncoding: "BLP_ALPHA_ENCODING_", - }.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def unpack_565(i): return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index c7c32252b..c46b2f28b 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -56,7 +56,6 @@ from enum import IntEnum from io import BytesIO from . import Image, ImageFile -from ._deprecate import deprecate MAGIC = b"FTEX" @@ -66,17 +65,6 @@ class Format(IntEnum): UNCOMPRESSED = 1 -def __getattr__(name): - for enum, prefix in {Format: "FORMAT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - class FtexImageFile(ImageFile.ImageFile): format = "FTEX" format_description = "Texture File Format (IW2:EOC)" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 99a895fb0..bc846fde7 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -59,22 +59,6 @@ from ._binary import i32le, o32be, o32le from ._deprecate import deprecate from ._util import DeferredError, is_path - -def __getattr__(name): - old_resampling = { - "LINEAR": "BILINEAR", - "CUBIC": "BICUBIC", - "ANTIALIAS": "LANCZOS", - } - if name in old_resampling: - deprecate( - name, 10, f"{old_resampling[name]} or Resampling.{old_resampling[name]}" - ) - return Resampling[old_resampling[name]] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - logger = logging.getLogger(__name__) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index f87849680..31b0e5a5e 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -20,8 +20,6 @@ from enum import IntEnum from PIL import Image -from ._deprecate import deprecate - try: from PIL import _imagingcms except ImportError as ex: @@ -117,17 +115,6 @@ class Direction(IntEnum): PROOF = 2 -def __getattr__(name): - for enum, prefix in {Intent: "INTENT_", Direction: "DIRECTION_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - # # flags diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9cdad2961..34a04a6bf 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -43,17 +43,6 @@ class Layout(IntEnum): RAQM = 1 -def __getattr__(name): - for enum, prefix in {Layout: "LAYOUT_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - try: from . import _imagingft as core except ImportError as ex: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 15a3c8291..82a74b267 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -45,7 +45,6 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 -from ._deprecate import deprecate logger = logging.getLogger(__name__) @@ -131,17 +130,6 @@ class Blend(IntEnum): """ -def __getattr__(name): - for enum, prefix in {Disposal: "APNG_DISPOSE_", Blend: "APNG_BLEND_"}.items(): - if name.startswith(prefix): - name = name[len(prefix) :] - if name in enum.__members__: - deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}") - return enum[name] - msg = f"module '{__name__}' has no attribute '{name}'" - raise AttributeError(msg) - - def _safe_zlib_decompress(s): dobj = zlib.decompressobj() plaintext = dobj.decompress(s, MAX_TEXT_CHUNK) From 575a038f9762ab142948d6d36a4ec3a37b8f7833 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:32:18 +0300 Subject: [PATCH 201/512] Remove FitsStubImagePlugin, deprecated in 9.1.0 --- Tests/test_file_fits.py | 38 +---------------- docs/deprecations.rst | 18 ++++---- src/PIL/FitsStubImagePlugin.py | 76 ---------------------------------- src/PIL/__init__.py | 1 - 4 files changed, 10 insertions(+), 123 deletions(-) delete mode 100644 src/PIL/FitsStubImagePlugin.py diff --git a/Tests/test_file_fits.py b/Tests/test_file_fits.py index 6f988729f..68b3eb567 100644 --- a/Tests/test_file_fits.py +++ b/Tests/test_file_fits.py @@ -2,7 +2,7 @@ from io import BytesIO import pytest -from PIL import FitsImagePlugin, FitsStubImagePlugin, Image +from PIL import FitsImagePlugin, Image from .helper import assert_image_equal, hopper @@ -48,39 +48,3 @@ 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 - loaded = False - - def open(self, im): - self.opened = True - - def load(self, im): - self.loaded = True - im.fp.close() - return Image.new("RGB", (1, 1)) - - handler = Handler() - with pytest.warns(DeprecationWarning): - FitsStubImagePlugin.register_handler(handler) - - with Image.open(TEST_FILE) as im: - assert im.format == "FITS" - assert im.size == (128, 128) - assert im.mode == "L" - - assert handler.opened - assert not handler.loaded - - im.load() - assert handler.loaded - - FitsStubImagePlugin._handler = None - Image.register_open( - FitsImagePlugin.FitsImageFile.format, - FitsImagePlugin.FitsImageFile, - FitsImagePlugin._accept, - ) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 8606ede4d..577cd6c27 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,15 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FitsStubImagePlugin -~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.1.0 - -The stub image plugin ``FitsStubImagePlugin`` has been deprecated and will be removed in -Pillow 10.0.0 (2023-07-01). FITS images can be read without a handler through -:mod:`~PIL.FitsImagePlugin` instead. - FreeTypeFont.getmask2 fill parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,6 +198,15 @@ Removed Use instead ``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` ===================================================== ============================================================ +FitsStubImagePlugin +~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.1.0 +.. versionremoved:: 10.0.0 + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/FitsStubImagePlugin.py b/src/PIL/FitsStubImagePlugin.py deleted file mode 100644 index 50948ec42..000000000 --- a/src/PIL/FitsStubImagePlugin.py +++ /dev/null @@ -1,76 +0,0 @@ -# -# The Python Imaging Library -# $Id$ -# -# FITS stub adapter -# -# Copyright (c) 1998-2003 by Fredrik Lundh -# -# See the README file for information on usage and redistribution. -# - -from . import FitsImagePlugin, Image, ImageFile -from ._deprecate import deprecate - -_handler = None - - -def register_handler(handler): - """ - Install application-specific FITS image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - deprecate( - "FitsStubImagePlugin", - 10, - action="FITS images can now be read without " - "a handler through FitsImagePlugin instead", - ) - - # Override FitsImagePlugin with this handler - # for backwards compatibility - try: - Image.ID.remove(FITSStubImageFile.format) - except ValueError: - pass - - Image.register_open( - FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept - ) - - -class FITSStubImageFile(ImageFile.StubImageFile): - format = FitsImagePlugin.FitsImageFile.format - format_description = FitsImagePlugin.FitsImageFile.format_description - - def _open(self): - offset = self.fp.tell() - - im = FitsImagePlugin.FitsImageFile(self.fp) - self._size = im.size - self.mode = im.mode - self.tile = [] - - self.fp.seek(offset) - - loader = self._load() - if loader: - loader.open(self) - - def _load(self): - return _handler - - -def _save(im, fp, filename): - msg = "FITS save handler not installed" - raise OSError(msg) - - -# -------------------------------------------------------------------- -# Registry - -Image.register_save(FITSStubImageFile.format, _save) diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py index 32d2381f3..2bb8f6d7f 100644 --- a/src/PIL/__init__.py +++ b/src/PIL/__init__.py @@ -31,7 +31,6 @@ _plugins = [ "DdsImagePlugin", "EpsImagePlugin", "FitsImagePlugin", - "FitsStubImagePlugin", "FliImagePlugin", "FpxImagePlugin", "FtexImagePlugin", From c9f11565f1be05578f831e8f2050a2164683a70e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:40:39 +0300 Subject: [PATCH 202/512] Remove FreeTypeFont.getmask2 fill parameter, deprecated in 9.2.0 --- Tests/test_imagefont.py | 8 -------- docs/deprecations.rst | 17 +++++++++-------- src/PIL/ImageFont.py | 16 +--------------- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 2d83b5a37..ca76ca6b2 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1083,14 +1083,6 @@ def test_woff2(layout_engine): assert_image_similar_tofile(im, "Tests/images/test_woff2.png", 5) -def test_fill_deprecation(font): - with pytest.warns(DeprecationWarning): - font.getmask2("Hello world", fill=Image.core.fill) - with pytest.warns(DeprecationWarning): - with pytest.raises(TypeError): - font.getmask2("Hello world", fill=None) - - def test_render_mono_size(): # issue 4177 diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 577cd6c27..eaef94907 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -FreeTypeFont.getmask2 fill parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been -deprecated and will be removed in Pillow 10 (2023-07-01). - PhotoImage.paste box parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,6 +199,15 @@ FitsStubImagePlugin The stub image plugin ``FitsStubImagePlugin`` has been removed. FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. +FreeTypeFont.getmask2 fill parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 34a04a6bf..e7d45636c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -51,9 +51,6 @@ except ImportError as ex: core = DeferredError(ex) -_UNSPECIFIED = object() - - # FIXME: add support for pilfont2 format (see FontFile.py) # -------------------------------------------------------------------- @@ -654,7 +651,6 @@ class FreeTypeFont: self, text, mode="", - fill=_UNSPECIFIED, direction=None, features=None, language=None, @@ -680,12 +676,6 @@ class FreeTypeFont: .. versionadded:: 1.1.5 - :param fill: Optional fill function. By default, an internal Pillow function - will be used. - - Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). - :param direction: Direction of the text. It can be 'rtl' (right to left), 'ltr' (left to right) or 'ttb' (top to bottom). Requires libraqm. @@ -738,10 +728,6 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - if fill is _UNSPECIFIED: - fill = Image.core.fill - else: - deprecate("fill", 10) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -750,7 +736,7 @@ class FreeTypeFont: size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) offset = offset[0] - stroke_width, offset[1] - stroke_width Image._decompression_bomb_check(size) - im = fill("RGBA" if mode == "RGBA" else "L", size, 0) + im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) if min(size): self.font.render( text, From 584f8c39de3265440b920f3814a9a06ef420dcd7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 21:54:51 +0300 Subject: [PATCH 203/512] Remove PhotoImage.paste() box parameter, deprecated in 9.2.0 --- Tests/test_imagetk.py | 7 ------- docs/deprecations.rst | 15 ++++++++------- src/PIL/ImageTk.py | 9 +-------- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index 995d0ee1f..a0c9574ba 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -89,13 +89,6 @@ def test_photoimage_blank(mode): assert_image_equal(reloaded.convert(mode), im) -def test_box_deprecation(): - im = hopper() - im_tk = ImageTk.PhotoImage(im) - with pytest.warns(DeprecationWarning): - im_tk.paste(im, (0, 0, 128, 128)) - - def test_bitmapimage(): im = hopper("1") diff --git a/docs/deprecations.rst b/docs/deprecations.rst index eaef94907..9b4186d01 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,13 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -PhotoImage.paste box parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -The ``box`` parameter is unused. It will be removed in Pillow 10.0.0 (2023-07-01). - Image.coerce_e ~~~~~~~~~~~~~~ @@ -208,6 +201,14 @@ FreeTypeFont.getmask2 fill parameter The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been removed. +PhotoImage.paste box parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +The ``box`` parameter was unused and has been removed. + PyQt5 and PySide2 ~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/ImageTk.py b/src/PIL/ImageTk.py index ef569ed2e..bf98eb2c8 100644 --- a/src/PIL/ImageTk.py +++ b/src/PIL/ImageTk.py @@ -29,7 +29,6 @@ import tkinter from io import BytesIO from . import Image -from ._deprecate import deprecate # -------------------------------------------------------------------- # Check for Tkinter interface hooks @@ -162,7 +161,7 @@ class PhotoImage: """ return self.__size[1] - def paste(self, im, box=None): + def paste(self, im): """ Paste a PIL image into the photo image. Note that this can be very slow if the photo image is displayed. @@ -170,13 +169,7 @@ class PhotoImage: :param im: A PIL image. The size must match the target region. If the mode does not match, the image is converted to the mode of the bitmap image. - :param box: Deprecated. This parameter will be removed in Pillow 10 - (2023-07-01). """ - - if box is not None: - deprecate("The box parameter", 10, None) - # convert to blittable im.load() image = im.im From b25bf5161a088d17a2ee7ebb509b866683519dad Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 22:16:13 +0300 Subject: [PATCH 204/512] Remove Image.coerce_e, deprecated in 9.2.0 --- Tests/test_image_point.py | 7 ------- docs/deprecations.rst | 16 ++++++++-------- src/PIL/Image.py | 25 ++++++++----------------- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/Tests/test_image_point.py b/Tests/test_image_point.py index 157ecb120..c406cb8ec 100644 --- a/Tests/test_image_point.py +++ b/Tests/test_image_point.py @@ -1,7 +1,5 @@ import pytest -from PIL import Image - from .helper import assert_image_equal, hopper @@ -62,8 +60,3 @@ def test_f_mode(): im = hopper("F") with pytest.raises(ValueError): im.point(None) - - -def test_coerce_e_deprecation(): - with pytest.warns(DeprecationWarning): - assert Image.coerce_e(2).data == 2 diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 9b4186d01..0eb3d6b41 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,14 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -Image.coerce_e -~~~~~~~~~~~~~~ - -.. deprecated:: 9.2.0 - -This undocumented method has been deprecated and will be removed in Pillow 10 -(2023-07-01). - .. _Font size and offset methods: Font size and offset methods @@ -222,6 +214,14 @@ Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or `PySide6 `_ instead. +Image.coerce_e +~~~~~~~~~~~~~~ + +.. deprecated:: 9.2.0 +.. versionremoved:: 10.0.0 + +This undocumented method has been removed. + PILLOW_VERSION constant ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bc846fde7..0d7a9bbfc 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -56,7 +56,6 @@ from . import ( _plugins, ) from ._binary import i32le, o32be, o32le -from ._deprecate import deprecate from ._util import DeferredError, is_path logger = logging.getLogger(__name__) @@ -421,26 +420,18 @@ def _getencoder(mode, encoder_name, args, extra=()): # Simple expression analyzer -def coerce_e(value): - deprecate("coerce_e", 10) - return value if isinstance(value, _E) else _E(1, value) - - -# _E(scale, offset) represents the affine transformation scale * x + offset. -# The "data" field is named for compatibility with the old implementation, -# and should be renamed once coerce_e is removed. class _E: - def __init__(self, scale, data): + def __init__(self, scale, offset): self.scale = scale - self.data = data + self.offset = offset def __neg__(self): - return _E(-self.scale, -self.data) + return _E(-self.scale, -self.offset) def __add__(self, other): if isinstance(other, _E): - return _E(self.scale + other.scale, self.data + other.data) - return _E(self.scale, self.data + other) + return _E(self.scale + other.scale, self.offset + other.offset) + return _E(self.scale, self.offset + other) __radd__ = __add__ @@ -453,19 +444,19 @@ class _E: def __mul__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale * other, self.data * other) + return _E(self.scale * other, self.offset * other) __rmul__ = __mul__ def __truediv__(self, other): if isinstance(other, _E): return NotImplemented - return _E(self.scale / other, self.data / other) + return _E(self.scale / other, self.offset / other) def _getscaleoffset(expr): a = expr(_E(1, 0)) - return (a.scale, a.data) if isinstance(a, _E) else (0, a) + return (a.scale, a.offset) if isinstance(a, _E) else (0, a) # -------------------------------------------------------------------- From f1f46a718d934fccd924483f147351c0d6a14fa7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 2 Apr 2023 23:41:16 +0300 Subject: [PATCH 205/512] Add removals to 10.0.0 release notes --- docs/releasenotes/10.0.0.rst | 149 +++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + docs/releasenotes/template.rst | 4 +- 3 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 docs/releasenotes/10.0.0.rst diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst new file mode 100644 index 000000000..cd0296e3e --- /dev/null +++ b/docs/releasenotes/10.0.0.rst @@ -0,0 +1,149 @@ +10.0.0 +------ + +Backwards Incompatible Changes +============================== + +Categories +^^^^^^^^^^ + +``im.category`` has been removed, along with the related ``Image.NORMAL``, +``Image.SEQUENCE`` and ``Image.CONTAINER`` attributes. + +To determine if an image has multiple frames or not, +``getattr(im, "is_animated", False)`` can be used instead. + +Tk/Tcl 8.4 +^^^^^^^^^^ + +Support for Tk/Tcl 8.4 has been removed. + +JpegImagePlugin.convert_dict_qtables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Since deprecation in Pillow 8.3.0, the ``convert_dict_qtables`` method no longer +performed any operations on the data given to it, and has been removed. + +ImagePalette size parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before Pillow 8.3.0, ``ImagePalette`` required palette data of particular lengths by +default, and the ``size`` parameter could be used to override that. Pillow 8.3.0 +removed the default required length, also removing the need for the ``size`` parameter. + +ImageShow.Viewer.show_file file argument +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``file`` argument in :py:meth:`~PIL.ImageShow.Viewer.show_file()` has been +removed and replaced by ``path``. + +In effect, ``viewer.show_file("test.jpg")`` will continue to work unchanged. + +Constants +^^^^^^^^^ + +A number of constants have been removed. +Instead, ``enum.IntEnum`` classes have been added. + +===================================================== ============================================================ +Removed Use instead +===================================================== ============================================================ +``Image.LINEAR`` ``Image.BILINEAR`` or ``Image.Resampling.BILINEAR`` +``Image.CUBIC`` ``Image.BICUBIC`` or ``Image.Resampling.BICUBIC`` +``Image.ANTIALIAS`` ``Image.LANCZOS`` or ``Image.Resampling.LANCZOS`` +``ImageCms.INTENT_PERCEPTUAL`` ``ImageCms.Intent.PERCEPTUAL`` +``ImageCms.INTENT_RELATIVE_COLORMETRIC`` ``ImageCms.Intent.RELATIVE_COLORMETRIC`` +``ImageCms.INTENT_SATURATION`` ``ImageCms.Intent.SATURATION`` +``ImageCms.INTENT_ABSOLUTE_COLORIMETRIC`` ``ImageCms.Intent.ABSOLUTE_COLORIMETRIC`` +``ImageCms.DIRECTION_INPUT`` ``ImageCms.Direction.INPUT`` +``ImageCms.DIRECTION_OUTPUT`` ``ImageCms.Direction.OUTPUT`` +``ImageCms.DIRECTION_PROOF`` ``ImageCms.Direction.PROOF`` +``ImageFont.LAYOUT_BASIC`` ``ImageFont.Layout.BASIC`` +``ImageFont.LAYOUT_RAQM`` ``ImageFont.Layout.RAQM`` +``BlpImagePlugin.BLP_FORMAT_JPEG`` ``BlpImagePlugin.Format.JPEG`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED`` ``BlpImagePlugin.Encoding.UNCOMPRESSED`` +``BlpImagePlugin.BLP_ENCODING_DXT`` ``BlpImagePlugin.Encoding.DXT`` +``BlpImagePlugin.BLP_ENCODING_UNCOMPRESSED_RAW_RGBA`` ``BlpImagePlugin.Encoding.UNCOMPRESSED_RAW_RGBA`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT1`` ``BlpImagePlugin.AlphaEncoding.DXT1`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT3`` ``BlpImagePlugin.AlphaEncoding.DXT3`` +``BlpImagePlugin.BLP_ALPHA_ENCODING_DXT5`` ``BlpImagePlugin.AlphaEncoding.DXT5`` +``FtexImagePlugin.FORMAT_DXT1`` ``FtexImagePlugin.Format.DXT1`` +``FtexImagePlugin.FORMAT_UNCOMPRESSED`` ``FtexImagePlugin.Format.UNCOMPRESSED`` +``PngImagePlugin.APNG_DISPOSE_OP_NONE`` ``PngImagePlugin.Disposal.OP_NONE`` +``PngImagePlugin.APNG_DISPOSE_OP_BACKGROUND`` ``PngImagePlugin.Disposal.OP_BACKGROUND`` +``PngImagePlugin.APNG_DISPOSE_OP_PREVIOUS`` ``PngImagePlugin.Disposal.OP_PREVIOUS`` +``PngImagePlugin.APNG_BLEND_OP_SOURCE`` ``PngImagePlugin.Blend.OP_SOURCE`` +``PngImagePlugin.APNG_BLEND_OP_OVER`` ``PngImagePlugin.Blend.OP_OVER`` +===================================================== ============================================================ + +FitsStubImagePlugin +^^^^^^^^^^^^^^^^^^^ + +The stub image plugin ``FitsStubImagePlugin`` has been removed. +FITS images can be read without a handler through :mod:`~PIL.FitsImagePlugin` instead. + +FreeTypeFont.getmask2 fill parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The undocumented ``fill`` parameter of :py:meth:`.FreeTypeFont.getmask2` has been +removed. + +PhotoImage.paste box parameter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``box`` parameter was unused and has been removed. + +PyQt5 and PySide2 +^^^^^^^^^^^^^^^^^ + +`Qt 5 reached end-of-life `_ on 2020-12-08 for +open-source users (and will reach EOL on 2023-12-08 for commercial licence holders). + +Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to +`PyQt6 `_ or +`PySide6 `_ instead. + +Image.coerce_e +^^^^^^^^^^^^^^ + +This undocumented method has been removed. + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 177fb65dd..9bca98541 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 + 10.0.0 9.5.0 9.4.0 9.3.0 diff --git a/docs/releasenotes/template.rst b/docs/releasenotes/template.rst index f7271ae2b..440d04b1c 100644 --- a/docs/releasenotes/template.rst +++ b/docs/releasenotes/template.rst @@ -1,5 +1,5 @@ -x.y.z ------ +xx.y.z +------ Backwards Incompatible Changes ============================== From f707e8abd6cc820c62923056b17af9139f6d8b3d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:40:31 +1000 Subject: [PATCH 206/512] 1 mode still fails for PyQt6 --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 399df670f..d071838f7 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -28,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT5 + # BW appears to not save correctly on QT # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From 2cba9904db9df42a1c1502296e567a2d000df3b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:48:06 +1000 Subject: [PATCH 207/512] Removed _category --- src/PIL/Image.py | 2 -- src/PIL/MicImagePlugin.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0d7a9bbfc..5a43f6c4a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -487,7 +487,6 @@ class Image: self._size = (0, 0) self.palette = None self.info = {} - self._category = 0 self.readonly = 0 self.pyaccess = None self._exif = None @@ -604,7 +603,6 @@ class Image: and self.mode == other.mode and self.size == other.size and self.info == other.info - and self._category == other._category and self.getpalette() == other.getpalette() and self.tobytes() == other.tobytes() ) diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 58f7327bd..801318930 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -66,9 +66,6 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 - if len(self.images) > 1: - self._category = Image.CONTAINER - self.seek(0) def seek(self, frame): From c26b4621597e56c60f54707edde4ca4c88f4cfd7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 11:52:38 +1000 Subject: [PATCH 208/512] Updated documentation --- docs/reference/Image.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 0eba1141a..35a4c2110 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -412,18 +412,6 @@ See :ref:`concept-filters` for details. :undoc-members: :noindex: -Some deprecated filters are also available under the following names: - -.. data:: NONE - :noindex: - :value: Resampling.NEAREST -.. data:: LINEAR - :value: Resampling.BILINEAR -.. data:: CUBIC - :value: Resampling.BICUBIC -.. data:: ANTIALIAS - :value: Resampling.LANCZOS - Dither modes ^^^^^^^^^^^^ From 94aa76ebdeb33be85a6c23d907877b7d45032ba2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 10:29:28 +0300 Subject: [PATCH 209/512] Fix typo --- Tests/test_qt_image_toqimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index d071838f7..95c13ba75 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -28,7 +28,7 @@ def test_sanity(mode, tmp_path): assert_image_equal(rt, src) if mode == "1": - # BW appears to not save correctly on QT + # BW appears to not save correctly on Qt # kicks out errors on console: # libpng warning: Invalid color type/bit depth combination # in IHDR From aa6c0dcc9e0c3d60ab77ced04f7c0e7e7d1f0fc0 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 10:31:47 +0300 Subject: [PATCH 210/512] Cygwin doesn't provide any Qt6 packages Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-cygwin.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 397cfe0a1..14a5f2c14 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -67,7 +67,6 @@ jobs: python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter - qt6-devel-tools wget xorg-server-extra zlib-devel From 4ffbbe194c5a1b8840f809574017ab5f1333695f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 8 Apr 2023 20:14:12 +1000 Subject: [PATCH 211/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 547893d82..2088a1da5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Remove deprecations for Pillow 10.0.0 #7059 + [hugovk, radarhere] + - Drop support for soon-EOL Python 3.7 #7058 [hugovk, radarhere] From 6797e47458177020877bf1efa3bed230e0db6d69 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 19:52:04 +0300 Subject: [PATCH 212/512] Add release check to make sure no TODOs remain in release notes --- Makefile | 1 + Tests/check_release_notes.py | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 Tests/check_release_notes.py diff --git a/Makefile b/Makefile index f51325d47..e41f36411 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,7 @@ debug: .PHONY: release-test release-test: + python3 Tests/check_release_notes.py python3 -m pip install -e .[tests] python3 selftest.py python3 -m pytest Tests diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py new file mode 100644 index 000000000..db9ba364b --- /dev/null +++ b/Tests/check_release_notes.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +for rst in Path("docs/releasenotes/").rglob("[1-9]*.rst"): + if "TODO" in open(rst).read(): + sys.exit(f"Error: remove TODO from {rst}") From 80a1238e2bf5796145b4d976524a52c603bd53eb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 9 Apr 2023 22:43:36 +0300 Subject: [PATCH 213/512] Simplify glob Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- Tests/check_release_notes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/check_release_notes.py b/Tests/check_release_notes.py index db9ba364b..0a9a898d7 100644 --- a/Tests/check_release_notes.py +++ b/Tests/check_release_notes.py @@ -1,6 +1,6 @@ import sys from pathlib import Path -for rst in Path("docs/releasenotes/").rglob("[1-9]*.rst"): +for rst in Path("docs/releasenotes").glob("[1-9]*.rst"): if "TODO" in open(rst).read(): sys.exit(f"Error: remove TODO from {rst}") From dd15f15d08bf3fd32c41ef9f2502286778c9f993 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 09:06:20 +1000 Subject: [PATCH 214/512] Added further field sizes --- Tests/test_file_tiff.py | 9 ++++++++- src/PIL/TiffImagePlugin.py | 4 ++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..43181d1b3 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -96,10 +96,17 @@ class TestFileTiff: assert_image_similar_tofile(im, "Tests/images/pil136.png", 1) - def test_bigtiff(self): + def test_bigtiff(self, tmp_path): with Image.open("Tests/images/hopper_bigtiff.tif") as im: assert_image_equal_tofile(im, "Tests/images/hopper.tif") + with Image.open("Tests/images/hopper_bigtiff.tif") as im: + # multistrip support not yet implemented + del im.tag_v2[273] + + outfile = str(tmp_path / "temp.tif") + im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2) + def test_set_legacy_api(self): ifd = TiffImagePlugin.ImageFileDirectory_v2() with pytest.raises(Exception) as e: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..5b7d3f302 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1892,6 +1892,10 @@ class AppendingTiffWriter: 8, # srational 4, # float 8, # double + 4, # ifd + 2, # unicode + 4, # complex + 8, # long8 ] # StripOffsets = 273 From b2301d70d104f36a08ae658f569d02f7796fc8fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 19:10:51 +1000 Subject: [PATCH 215/512] Removed ImageFont.getsize and related functions --- Tests/images/imagedraw_stroke_multiline.png | Bin 4061 -> 4059 bytes Tests/test_deprecate.py | 19 +- Tests/test_font_pcf.py | 3 - Tests/test_imagedraw.py | 19 +- Tests/test_imagedraw2.py | 16 -- Tests/test_imagefont.py | 95 +--------- docs/deprecations.rst | 98 +++++----- docs/releasenotes/10.0.0.rst | 16 ++ docs/releasenotes/9.2.0.rst | 20 +- src/PIL/ImageDraw.py | 97 +--------- src/PIL/ImageDraw2.py | 16 -- src/PIL/ImageFont.py | 193 -------------------- src/PIL/_deprecate.py | 2 - 13 files changed, 96 insertions(+), 498 deletions(-) diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png index fc5e07c8679d5aa12a1d95152ea6d2542f169697..c290fc0568e5c4b40299a208cde7d0d760077fd0 100644 GIT binary patch delta 3993 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 delta 3990 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 waL7gnG#ZUYqtR$Ik*ODxum}VhG*>` for more information. - - Returns width and height (in pixels) of given text. - - :param text: Text to measure. - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - return self.font.getsize(text) - def getmask(self, text, mode="", *args, **kwargs): """ Create a bitmap for the text. @@ -398,165 +380,6 @@ class FreeTypeFont: width, height = size[0] + 2 * stroke_width, size[1] + 2 * stroke_width return left, top, left + width, top + height - def getsize( - self, - text, - direction=None, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`getlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`getbbox()` to get the exact bounding box based on an anchor. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font with - provided direction, features, and language. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use the bottom value of :meth:`getbbox` with ``anchor='lt'`` instead. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize", 10, "getbbox or getlength") - # vertical offset is added for historical reasons - # see https://github.com/python-pillow/Pillow/pull/4910#discussion_r486682929 - size, offset = self.font.getsize(text, "L", direction, features, language) - return ( - size[0] + stroke_width * 2, - size[1] + stroke_width * 2 + offset[1], - ) - - def getsize_multiline( - self, - text, - direction=None, - spacing=4, - features=None, - language=None, - stroke_width=0, - ): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.ImageDraw.multiline_textbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns width and height (in pixels) of given text if rendered in font - with provided direction, features, and language, while respecting - newline characters. - - :param text: Text to measure. - - :param direction: Direction of the text. It can be 'rtl' (right to - left), 'ltr' (left to right) or 'ttb' (top to bottom). - Requires libraqm. - - :param spacing: The vertical gap between lines, defaulting to 4 pixels. - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example 'dlig' or 'ss01', but can be also - used to turn off default font features for - example '-liga' to disable ligatures or '-kern' - to disable kerning. To get all supported - features, see - https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist - Requires libraqm. - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code - `_ - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - """ - deprecate("getsize_multiline", 10, "ImageDraw.multiline_textbbox") - max_width = 0 - lines = self._multiline_split(text) - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - line_spacing = self.getsize("A", stroke_width=stroke_width)[1] + spacing - for line in lines: - line_width, line_height = self.getsize( - line, direction, features, language, stroke_width - ) - max_width = max(max_width, line_width) - - return max_width, len(lines) * line_spacing - spacing - - def getoffset(self, text): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` instead. - - See :ref:`deprecations ` for more information. - - Returns the offset of given text. This is the gap between the - starting coordinate and the first marking. Note that this gap is - included in the result of :py:func:`~PIL.ImageFont.FreeTypeFont.getsize`. - - :param text: Text to measure. - - :return: A tuple of the x and y offset - """ - deprecate("getoffset", 10, "getbbox") - return self.font.getsize(text)[1] - def getmask( self, text, @@ -851,22 +674,6 @@ class TransposedFont: self.font = font self.orientation = orientation # any 'transpose' argument, or None - def getsize(self, text, *args, **kwargs): - """ - .. deprecated:: 9.2.0 - - Use :py:meth:`.getbbox` or :py:meth:`.getlength` instead. - - See :ref:`deprecations ` for more information. - """ - deprecate("getsize", 10, "getbbox or getlength") - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - w, h = self.font.getsize(text) - if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): - return h, w - return w, h - def getmask(self, text, mode="", *args, **kwargs): im = self.font.getmask(text, mode, *args, **kwargs) if self.orientation is not None: diff --git a/src/PIL/_deprecate.py b/src/PIL/_deprecate.py index 81f2189dc..2f2a3df13 100644 --- a/src/PIL/_deprecate.py +++ b/src/PIL/_deprecate.py @@ -45,8 +45,6 @@ def deprecate( elif when <= int(__version__.split(".")[0]): msg = f"{deprecated} {is_} deprecated and should be removed." raise RuntimeError(msg) - elif when == 10: - removed = "Pillow 10 (2023-07-01)" elif when == 11: removed = "Pillow 11 (2024-10-15)" else: From adbb04d5dca5ae4f59ce51ca7474a3918e1f377d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 5 Apr 2023 12:32:13 +0300 Subject: [PATCH 216/512] Formatting for readability --- Tests/test_imagefont.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index f43044efe..a614b0fe5 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -306,7 +306,10 @@ def test_rotated_transposed_font(font, orientation): bbox_b = draw.textbbox((20, 20), word) # Check (w,h) of box a is (h,w) of box b - assert (bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1]) == ( + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( bbox_b[3] - bbox_b[1], bbox_b[2] - bbox_b[0], ) @@ -349,7 +352,10 @@ def test_unrotated_transposed_font(font, orientation): length_b = draw.textlength(word) # Check boxes a and b are same size - assert (bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1]) == ( + assert ( + bbox_a[2] - bbox_a[0], + bbox_a[3] - bbox_a[1], + ) == ( bbox_b[2] - bbox_b[0], bbox_b[3] - bbox_b[1], ) From b27794fc01c84b876fe876e8ebbd8b4b6a4f78ce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 17:24:16 +1000 Subject: [PATCH 217/512] Added test for ImageDraw2 textbbox --- Tests/test_imagedraw2.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Tests/test_imagedraw2.py b/Tests/test_imagedraw2.py index 2a5219893..a8a2ee1fc 100644 --- a/Tests/test_imagedraw2.py +++ b/Tests/test_imagedraw2.py @@ -2,7 +2,7 @@ import os.path import pytest -from PIL import Image, ImageDraw, ImageDraw2 +from PIL import Image, ImageDraw, ImageDraw2, features from .helper import ( assert_image_equal, @@ -170,6 +170,21 @@ def test_text(): assert_image_similar_tofile(im, expected, 13) +@skip_unless_feature("freetype2") +def test_textbbox(): + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw2.Draw(im) + font = ImageDraw2.Font("white", FONT_PATH) + + # Act + bbox = draw.textbbox((0, 0), "ImageDraw2", font) + + # Assert + right = 72 if features.check_feature("raqm") else 70 + assert bbox == (0, 2, right, 12) + + @skip_unless_feature("freetype2") def test_textsize_empty_string(): # Arrange From fa6cd4a19519c7c6af06265e39dc5f0e51fdd734 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 17:34:33 +1000 Subject: [PATCH 218/512] Only check width and height of transposed fonts once --- Tests/test_imagefont.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index a614b0fe5..623365d53 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -305,7 +305,7 @@ def test_rotated_transposed_font(font, orientation): draw.font = transposed_font bbox_b = draw.textbbox((20, 20), word) - # Check (w,h) of box a is (h,w) of box b + # Check (w, h) of box a is (h, w) of box b assert ( bbox_a[2] - bbox_a[0], bbox_a[3] - bbox_a[1], @@ -314,11 +314,8 @@ def test_rotated_transposed_font(font, orientation): bbox_b[2] - bbox_b[0], ) - # Check bbox b is (20, 20, 20 + h, 20 + w) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[3] - bbox_a[1] - assert bbox_b[3] == 20 + bbox_a[2] - bbox_a[0] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) # text length is undefined for vertical text with pytest.raises(ValueError): @@ -360,11 +357,8 @@ def test_unrotated_transposed_font(font, orientation): bbox_b[3] - bbox_b[1], ) - # Check bbox b is (20, 20, 20 + w, 20 + h) - assert bbox_b[0] == 20 - assert bbox_b[1] == 20 - assert bbox_b[2] == 20 + bbox_a[2] - bbox_a[0] - assert bbox_b[3] == 20 + bbox_a[3] - bbox_a[1] + # Check top left co-ordinates are correct + assert bbox_b[:2] == (20, 20) assert length_a == length_b From bc0bf5efea357a18c8f10a0c5768e837891b808e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 18:47:58 +1000 Subject: [PATCH 219/512] Preserve line spacing backwards compatibility --- Tests/images/imagedraw_stroke_multiline.png | Bin 4059 -> 4061 bytes Tests/test_imagedraw.py | 4 ++-- src/PIL/ImageDraw.py | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/images/imagedraw_stroke_multiline.png b/Tests/images/imagedraw_stroke_multiline.png index c290fc0568e5c4b40299a208cde7d0d760077fd0..fc5e07c8679d5aa12a1d95152ea6d2542f169697 100644 GIT binary patch delta 3990 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 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 17ee75dfa..7ffd7969d 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1220,8 +1220,8 @@ def test_textbbox_stroke(): # Act / Assert assert draw.textbbox((2, 2), "A", font, stroke_width=2) == (0, 4, 16, 20) assert draw.textbbox((2, 2), "A", font, stroke_width=4) == (-2, 2, 18, 22) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 42) - assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 46) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=2) == (0, 4, 52, 44) + assert draw.textbbox((2, 2), "ABC\nAaaa", font, stroke_width=4) == (-2, 2, 54, 50) @skip_unless_feature("freetype2") diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c16ff28cc..e9ccf8041 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -431,7 +431,11 @@ class ImageDraw: return text.split(split_character) def _multiline_spacing(self, font, spacing, stroke_width): - return self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + spacing + return ( + self.textbbox((0, 0), "A", font, stroke_width=stroke_width)[3] + + stroke_width + + spacing + ) def text( self, From fe4e52deacf1be96bb69758739d7fc88967a12e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Apr 2023 16:15:42 +1000 Subject: [PATCH 220/512] Rearranged code --- setup.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/setup.py b/setup.py index d780b038a..453ea688f 100755 --- a/setup.py +++ b/setup.py @@ -473,17 +473,15 @@ class pil_build_ext(build_ext): lib_root = include_root = root if lib_root is not None: - if isinstance(lib_root, str): - _add_directory(library_dirs, lib_root) - else: - for lib_dir in lib_root: - _add_directory(library_dirs, lib_dir) + if not isinstance(lib_root, (tuple, list)): + lib_root = (lib_root,) + for lib_dir in lib_root: + _add_directory(library_dirs, lib_dir) if include_root is not None: - if isinstance(include_root, str): - _add_directory(include_dirs, include_root) - else: - for include_dir in include_root: - _add_directory(include_dirs, include_dir) + if not isinstance(include_root, (tuple, list)): + include_root = (include_root,) + for include_dir in include_root: + _add_directory(include_dirs, include_dir) # respect CFLAGS/CPPFLAGS/LDFLAGS for k in ("CFLAGS", "CPPFLAGS", "LDFLAGS"): From 16aa710c7804c24b8518d50927de92aac7cf5b1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 20:14:15 +1000 Subject: [PATCH 221/512] Updated documentation --- docs/deprecations.rst | 2 - docs/reference/ImageDraw.rst | 110 ----------------------------------- 2 files changed, 112 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 64d0569c8..45b2f4200 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -12,8 +12,6 @@ Deprecated features Below are features which are considered deprecated. Where appropriate, a ``DeprecationWarning`` is issued. -.. _Font size and offset methods: - PSFile ~~~~~~ diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 43a5a2bc2..aec7a3ef8 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -474,116 +474,6 @@ Methods .. versionadded:: 8.0.0 -.. py:method:: ImageDraw.textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - Return the size of the given string, in pixels. - - .. note:: For historical reasons this function measures text height from - the ascender line instead of the top, see :ref:`text-anchors`. - If you wish to measure text height from the top, it is recommended - to use :meth:`textbbox` with ``anchor='lt'`` instead. - - :param text: Text to be measured. If it contains any newline characters, - the text is passed on to :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: If the text is passed on to - :py:meth:`~PIL.ImageDraw.ImageDraw.multiline_textsize`, - the number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - -.. py:method:: ImageDraw.multiline_textsize(text, font=None, spacing=4, direction=None, features=None, language=None, stroke_width=0) - - .. deprecated:: 9.2.0 - - See :ref:`deprecations ` for more information. - - Use :py:meth:`.multiline_textbbox` instead. - - Return the size of the given string, in pixels. - - Use :py:meth:`textlength()` to measure the offset of following text with - 1/64 pixel precision. - Use :py:meth:`textbbox()` to get the exact bounding box based on an anchor. - - .. note:: For historical reasons this function measures text height as the - distance between the top ascender line and bottom descender line, - not the top and bottom of the text, see :ref:`text-anchors`. - If you wish to measure text height from the top to the bottom of text, - it is recommended to use :meth:`multiline_textbbox` instead. - - :param text: Text to be measured. - :param font: An :py:class:`~PIL.ImageFont.ImageFont` instance. - :param spacing: The number of pixels between lines. - :param direction: Direction of the text. It can be ``"rtl"`` (right to - left), ``"ltr"`` (left to right) or ``"ttb"`` (top to bottom). - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param features: A list of OpenType font features to be used during text - layout. This is usually used to turn on optional - font features that are not enabled by default, - for example ``"dlig"`` or ``"ss01"``, but can be also - used to turn off default font features, for - example ``"-liga"`` to disable ligatures or ``"-kern"`` - to disable kerning. To get all supported - features, see `OpenType docs`_. - Requires libraqm. - - .. versionadded:: 4.2.0 - - :param language: Language of the text. Different languages may use - different glyph shapes or ligatures. This parameter tells - the font which language the text is in, and to apply the - correct substitutions as appropriate, if available. - It should be a `BCP 47 language code`_. - Requires libraqm. - - .. versionadded:: 6.0.0 - - :param stroke_width: The width of the text stroke. - - .. versionadded:: 6.2.0 - - :return: (width, height) - .. py:method:: ImageDraw.textlength(text, font=None, direction=None, features=None, language=None, embedded_color=False) Returns length (in pixels with 1/64 precision) of given text when rendered From 5ef3ddafe30bfd64ff49f52f05c7a97712655a73 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 10 Apr 2023 20:56:46 +1000 Subject: [PATCH 222/512] Do not install PyQt6-Qt6 6.5.0 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 6aa122cc5..81631a9bd 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -43,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - python3 -m pip install pyqt6 + python3 -m pip install pyqt6 PyQt6-Qt6!=6.5.0 fi # webp From 900db50a74fd75a8a108e236a7940527da49149b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 17:16:02 +0300 Subject: [PATCH 223/512] Move 'git push --all' later --- RELEASING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index c203a9c12..2f28372ac 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -18,7 +18,6 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. ```bash git branch 5.2.x git tag 5.2.0 - git push --all git push --tags ``` * [ ] Create and check source distribution: @@ -32,8 +31,11 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` - +* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), + increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: + ```bash + git push --all + ``` ## Point Release Released as needed for security, installation or critical bug fixes. From 34908960f0a8f8b70756a232e1e754a987d6d634 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 17:11:20 +0300 Subject: [PATCH 224/512] Flip order to give slower macOS/Linux a headstart --- RELEASING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 2f28372ac..feb6f9469 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -97,11 +97,7 @@ Released as needed privately to individual vendors for critical security-related ## Binary Distributions -### Windows -* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` - -### Mac and Linux +### macOS and Linux * [ ] Use the [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels): ```bash git clone https://github.com/python-pillow/pillow-wheels @@ -111,6 +107,10 @@ Released as needed privately to individual vendors for critical security-related * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) and copy into `dist/` +### Windows +* [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) + and copy into `dist/` + ## Publicize Release * [ ] Announce release availability via [Twitter](https://twitter.com/pythonpillow) and [Mastodon](https://fosstodon.org/@pillow) e.g. https://twitter.com/PythonPillow/status/1013789184354603010 From c9ec517b4d2bd44cf7905d8fcb7e37c01c21541c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 18:56:37 +0300 Subject: [PATCH 225/512] Give examples how to download binary releases using gh CLI --- RELEASING.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index feb6f9469..48f7ea104 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -105,11 +105,18 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/` + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels + ``` ### Windows * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/` + and copy into `dist/`For example using [GitHub CLI](https://github.com/cli/cli): + ```bash + gh run download --dir dist + # select dist-x.y.z + ``` ## Publicize Release From 47ac51cd8fda3ef701926fbffe01eb914e936eff Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 8 Apr 2023 18:59:44 +0300 Subject: [PATCH 226/512] Update PEP links to redirects --- RELEASING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 48f7ea104..97f0144f8 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -11,7 +11,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. * [ ] Develop and prepare release in `main` branch. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in `main` branch. * [ ] Check that all of the wheel builds [Pillow Wheel Builder](https://github.com/python-pillow/pillow-wheels) pass the tests in Travis CI and GitHub Actions. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Update `CHANGES.rst`. * [ ] Run pre-release check via `make release-test` in a freshly cloned repo. * [ ] Create branch and tag for release e.g.: @@ -31,7 +31,7 @@ Released quarterly on January 2nd, April 1st, July 1st and October 15th. python3 -m twine upload dist/Pillow-5.2.0* ``` * [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), increment and append `.dev0` to version identifier in `src/PIL/_version.py` and then: ```bash git push --all @@ -51,7 +51,7 @@ Released as needed for security, installation or critical bug fixes. * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. -* [ ] In compliance with [PEP 440](https://www.python.org/dev/peps/pep-0440/), update version identifier in `src/PIL/_version.py` +* [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash From 6a3c3991395825b564779854f83afa9a285dd79c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 9 Apr 2023 22:39:24 +0300 Subject: [PATCH 227/512] Fix typo Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index 97f0144f8..ac9187ac9 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -112,7 +112,7 @@ Released as needed privately to individual vendors for critical security-related ### Windows * [ ] Download the artifacts from the [GitHub Actions "Test Windows" workflow](https://github.com/python-pillow/Pillow/actions/workflows/test-windows.yml) - and copy into `dist/`For example using [GitHub CLI](https://github.com/cli/cli): + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): ```bash gh run download --dir dist # select dist-x.y.z From 08b553957b0d4a8cad189534d6cf1a0dc8adc866 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Apr 2023 22:09:03 +1000 Subject: [PATCH 228/512] Move "git push" commands later --- RELEASING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index ac9187ac9..bb0d4e038 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -47,16 +47,12 @@ Released as needed for security, installation or critical bug fixes. git checkout -t remotes/origin/5.2.x ``` * [ ] Cherry pick individual commits from `main` branch to release branch e.g. `5.2.x`, then `git push`. - - - * [ ] Check [GitHub Actions](https://github.com/python-pillow/Pillow/actions) and [AppVeyor](https://ci.appveyor.com/project/python-pillow/Pillow) to confirm passing tests in release branch e.g. `5.2.x`. * [ ] In compliance with [PEP 440](https://peps.python.org/pep-0440/), update version identifier in `src/PIL/_version.py` * [ ] Run pre-release check via `make release-test`. * [ ] Create tag for release e.g.: ```bash git tag 5.2.1 - git push git push --tags ``` * [ ] Create and check source distribution: @@ -69,7 +65,10 @@ Released as needed for security, installation or critical bug fixes. python3 -m twine check --strict dist/* python3 -m twine upload dist/Pillow-5.2.1* ``` -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push + ``` ## Embargoed Release @@ -85,7 +84,6 @@ Released as needed privately to individual vendors for critical security-related ```bash git checkout 2.5.x git tag 2.5.3 - git push origin 2.5.x git push origin --tags ``` * [ ] Create and check source distribution: @@ -93,7 +91,10 @@ Released as needed privately to individual vendors for critical security-related make sdist ``` * [ ] Create [binary distributions](https://github.com/python-pillow/Pillow/blob/main/RELEASING.md#binary-distributions) -* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) +* [ ] Publish the [release on GitHub](https://github.com/python-pillow/Pillow/releases) and then: + ```bash + git push origin 2.5.x + ``` ## Binary Distributions From cb68187006e02b3aeda44160338b67891455749e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Mon, 10 Apr 2023 15:45:40 +0300 Subject: [PATCH 229/512] Clarify command should be run from main repo --- RELEASING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASING.md b/RELEASING.md index bb0d4e038..604bb1b8c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -106,7 +106,7 @@ Released as needed privately to individual vendors for critical security-related ./update-pillow-tag.sh [[release tag]] ``` * [ ] Download wheels from the [Pillow Wheel Builder release](https://github.com/python-pillow/pillow-wheels/releases) - and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli): + and copy into `dist/`. For example using [GitHub CLI](https://github.com/cli/cli) from the main repo: ```bash gh release download --dir dist --pattern "*.whl" --repo python-pillow/pillow-wheels ``` From 38c2449ef98f5634463fc06691bffd3c1213a527 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 07:31:43 +1000 Subject: [PATCH 230/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2088a1da5..f38cb5c0e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Remove deprecations for Pillow 10.0.0 #7059 +- Remove deprecations for Pillow 10.0.0 #7059, #7080 [hugovk, radarhere] - Drop support for soon-EOL Python 3.7 #7058 From 9aefa8d5eb41c78330d240f37f1cf175ac5379b3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 12:20:27 +1000 Subject: [PATCH 231/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f38cb5c0e..cd0b95085 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed type handling for include and lib directories #7069 + [adisbladis, radarhere] + - Remove deprecations for Pillow 10.0.0 #7059, #7080 [hugovk, radarhere] From 6fac6b15f2cf67a9d9d118e10e5bdd8622014f26 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 21:13:13 +1000 Subject: [PATCH 232/512] Revert "Do not install PyQt6-Qt6 6.5.0" This reverts commit 5ef3ddafe30bfd64ff49f52f05c7a97712655a73. --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 81631a9bd..6aa122cc5 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -43,7 +43,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - python3 -m pip install pyqt6 PyQt6-Qt6!=6.5.0 + python3 -m pip install pyqt6 fi # webp From 8aa61243e5300c4407ace4f016f448a8e9d648aa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 11 Apr 2023 21:13:49 +1000 Subject: [PATCH 233/512] Install libxcb-cursor0 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 6aa122cc5..17c349ab1 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 2d216d3d3d030a52750a22694eeab1eb92acc844 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Apr 2023 18:27:57 +0300 Subject: [PATCH 234/512] Don't install unused and deleted codecov --- .ci/after_success.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/after_success.sh b/.ci/after_success.sh index 23a6fcd4d..c71546f00 100755 --- a/.ci/after_success.sh +++ b/.ci/after_success.sh @@ -1,7 +1,7 @@ #!/bin/bash # gather the coverage data -python3 -m pip install codecov +python3 -m pip install coverage if [[ $MATRIX_DOCKER ]]; then python3 -m coverage xml --ignore-errors else From 3bb6344541a14f45ed1bfac3b0f7ff090be08423 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Apr 2023 18:12:21 +0300 Subject: [PATCH 235/512] Replace deleted codecov package with bash uploader --- .appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 9ed192e0f..47cbb55a8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -52,8 +52,9 @@ test_script: #- '%PYTHON%\%EXECUTABLE% test-installed.py -v -s %TEST_OPTIONS%' TODO TEST_OPTIONS with pytest? after_test: -- python -m pip install codecov -- codecov --file coverage.xml --name %PYTHON% --flags AppVeyor +- curl -Os https://uploader.codecov.io/latest/windows/codecov.exe +- chmod +x codecov +- .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: fast_finish: true From f619675115275a330b0d99660c4442fe7f7d3fe2 Mon Sep 17 00:00:00 2001 From: Nulano Date: Wed, 12 Apr 2023 21:14:38 +0200 Subject: [PATCH 236/512] Update vendored Raqm to 0.10.1 --- src/thirdparty/raqm/COPYING | 2 +- src/thirdparty/raqm/NEWS | 35 ++++++++++++++++++++++++++++++ src/thirdparty/raqm/README.md | 2 +- src/thirdparty/raqm/raqm-version.h | 4 ++-- src/thirdparty/raqm/raqm.c | 25 ++++++++++++++++----- src/thirdparty/raqm/raqm.h | 2 +- 6 files changed, 60 insertions(+), 10 deletions(-) diff --git a/src/thirdparty/raqm/COPYING b/src/thirdparty/raqm/COPYING index c605a5dc6..97e2489b7 100644 --- a/src/thirdparty/raqm/COPYING +++ b/src/thirdparty/raqm/COPYING @@ -1,7 +1,7 @@ The MIT License (MIT) Copyright © 2015 Information Technology Authority (ITA) -Copyright © 2016-2022 Khaled Hosny +Copyright © 2016-2023 Khaled Hosny Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/thirdparty/raqm/NEWS b/src/thirdparty/raqm/NEWS index ae1128485..e8bf32e0b 100644 --- a/src/thirdparty/raqm/NEWS +++ b/src/thirdparty/raqm/NEWS @@ -1,3 +1,38 @@ +Overview of changes leading to 0.10.1 +Wednesday, April 12, 2023 +==================================== + +Make combining marks always inherit the script of their base. + +Overview of changes leading to 0.10.0 +Wednesday, January 11, 2023 +==================================== + +Fix font feature ranges. + +Fix resolved direction for all-neutral text. + +Implement letter and word spacing support. + +New API: + * raqm_set_text_utf16 + +Overview of changes leading to 0.9.0 +Sunday, January 30, 2022 +==================================== + +Raise the minimum versions of Raqm dependencies: no longer conditionally +enabling any features based on specific dependency version. + +raqm_t objects can now be reused by calling raqm_clear_contents() before +re-use, to potentially reduce the number memory allocations. + +Don't hardcode python3 in tests. + +New API: + * raqm_set_freetype_load_flags_range + * raqm_clear_contents + Overview of changes leading to 0.8.0 Monday, December 13, 2021 ==================================== diff --git a/src/thirdparty/raqm/README.md b/src/thirdparty/raqm/README.md index 315e0c8d8..ab729cdc0 100644 --- a/src/thirdparty/raqm/README.md +++ b/src/thirdparty/raqm/README.md @@ -81,5 +81,5 @@ The following projects have patches to support complex text layout using Raqm: [1]: https://github.com/fribidi/fribidi [2]: https://github.com/Tehreer/SheenBidi [3]: https://github.com/harfbuzz/harfbuzz -[4]: https://www.freetype.org +[4]: https://freetype.org/ [5]: https://www.gtk.org/gtk-doc diff --git a/src/thirdparty/raqm/raqm-version.h b/src/thirdparty/raqm/raqm-version.h index bdb6fb662..62d2d2064 100644 --- a/src/thirdparty/raqm/raqm-version.h +++ b/src/thirdparty/raqm/raqm-version.h @@ -33,9 +33,9 @@ #define RAQM_VERSION_MAJOR 0 #define RAQM_VERSION_MINOR 10 -#define RAQM_VERSION_MICRO 0 +#define RAQM_VERSION_MICRO 1 -#define RAQM_VERSION_STRING "0.10.0" +#define RAQM_VERSION_STRING "0.10.1" #define RAQM_VERSION_ATLEAST(major,minor,micro) \ ((major)*10000+(minor)*100+(micro) <= \ diff --git a/src/thirdparty/raqm/raqm.c b/src/thirdparty/raqm/raqm.c index 770ea3018..2b331e1af 100644 --- a/src/thirdparty/raqm/raqm.c +++ b/src/thirdparty/raqm/raqm.c @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to @@ -1432,7 +1432,7 @@ raqm_get_glyphs (raqm_t *rq, * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_par_resolved_direction (raqm_t *rq) { if (!rq) @@ -1455,7 +1455,7 @@ raqm_get_par_resolved_direction (raqm_t *rq) * * Since: 0.8 */ -RAQM_API raqm_direction_t +raqm_direction_t raqm_get_direction_at_index (raqm_t *rq, size_t index) { @@ -2021,6 +2021,22 @@ _get_pair_index (const uint32_t ch) #define STACK_IS_EMPTY(script) ((script)->size <= 0) #define IS_OPEN(pair_index) (((pair_index) & 1) == 0) +static hb_script_t +_raqm_unicode_script (hb_codepoint_t u) +{ + static hb_unicode_funcs_t* unicode_funcs; + + unicode_funcs = hb_unicode_funcs_get_default (); + + /* Make combining marks inherit the script of their bases, regardless of + * their own script. + */ + if (hb_unicode_general_category (unicode_funcs, u) == HB_UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK) + return HB_SCRIPT_INHERITED; + + return hb_unicode_script (unicode_funcs, u); +} + /* Resolve the script for each character in the input string, if the character * script is common or inherited it takes the script of the character before it * except paired characters which we try to make them use the same script. We @@ -2033,10 +2049,9 @@ _raqm_resolve_scripts (raqm_t *rq) int last_set_index = -1; hb_script_t last_script = HB_SCRIPT_INVALID; _raqm_stack_t *stack = NULL; - hb_unicode_funcs_t* unicode_funcs = hb_unicode_funcs_get_default (); for (size_t i = 0; i < rq->text_len; ++i) - rq->text_info[i].script = hb_unicode_script (unicode_funcs, rq->text[i]); + rq->text_info[i].script = _raqm_unicode_script (rq->text[i]); #ifdef RAQM_TESTING RAQM_TEST ("Before script detection:\n"); diff --git a/src/thirdparty/raqm/raqm.h b/src/thirdparty/raqm/raqm.h index 2fd836c86..6fd6089c7 100644 --- a/src/thirdparty/raqm/raqm.h +++ b/src/thirdparty/raqm/raqm.h @@ -1,6 +1,6 @@ /* * Copyright © 2015 Information Technology Authority (ITA) - * Copyright © 2016-2022 Khaled Hosny + * Copyright © 2016-2023 Khaled Hosny * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to From 087a5f889a2e0dd350f7f586b357207e1055356b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 13 Apr 2023 07:00:07 +0300 Subject: [PATCH 237/512] Remove chmod Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 47cbb55a8..b2599e3d8 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -53,7 +53,6 @@ test_script: after_test: - curl -Os https://uploader.codecov.io/latest/windows/codecov.exe -- chmod +x codecov - .\codecov.exe --file coverage.xml --name %PYTHON% --flags AppVeyor matrix: From 86716ff2b094bb0b071208bdf7299d5eefa95085 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Apr 2023 07:52:32 +1000 Subject: [PATCH 238/512] Updated raqm to 0.10.1 --- depends/install_raqm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/depends/install_raqm.sh b/depends/install_raqm.sh index d1b31cfa5..24c1f9c30 100755 --- a/depends/install_raqm.sh +++ b/depends/install_raqm.sh @@ -2,7 +2,7 @@ # install raqm -archive=libraqm-0.10.0 +archive=libraqm-0.10.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz From accbd8ad93203d7c6068c75885e22f81e7164c98 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Apr 2023 11:13:28 +1000 Subject: [PATCH 239/512] Updated nasm to 2.16.01 --- .appveyor.yml | 4 ++-- .github/workflows/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index b2599e3d8..36f5bd0ad 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -26,9 +26,9 @@ install: - 7z x pillow-test-images.zip -oc:\ - mv c:\pillow-depends-main c:\pillow-depends - xcopy /S /Y c:\test-images-main\* c:\pillow\tests\images -- 7z x ..\pillow-depends\nasm-2.15.05-win64.zip -oc:\ +- 7z x ..\pillow-depends\nasm-2.16.01-win64.zip -oc:\ - choco install ghostscript --version=10.0.0.20230317 -- path c:\nasm-2.15.05;C:\Program Files\gs\gs10.00.0\bin;%PATH% +- path c:\nasm-2.16.01;C:\Program Files\gs\gs10.00.0\bin;%PATH% - cd c:\pillow\winbuild\ - ps: | c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 8f4d53ecf..a00880111 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -71,8 +71,8 @@ jobs: - name: Install dependencies id: install run: | - 7z x winbuild\depends\nasm-2.15.05-win64.zip "-o$env:RUNNER_WORKSPACE\" - echo "$env:RUNNER_WORKSPACE\nasm-2.15.05" >> $env:GITHUB_PATH + 7z x winbuild\depends\nasm-2.16.01-win64.zip "-o$env:RUNNER_WORKSPACE\" + echo "$env:RUNNER_WORKSPACE\nasm-2.16.01" >> $env:GITHUB_PATH choco install ghostscript --version=10.0.0.20230317 echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH From 73e94824888e6ed8019cc36099c92eea9b45e340 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 09:41:19 +1000 Subject: [PATCH 240/512] Select Python version --- .github/workflows/test-cygwin.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 14a5f2c14..6a5f73857 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -84,6 +84,10 @@ jobs: restore-keys: | ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + - name: Build system information run: | dash.exe -c "python3 .github/workflows/system-info.py" From 8d3014b8bf94ee6d07276b227dfac4d6f7f5a859 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 21:03:59 +1000 Subject: [PATCH 241/512] Added inPlace argument to exif_transpose() --- Tests/images/orientation_rectangle.jpg | Bin 0 -> 669 bytes Tests/test_imageops.py | 12 ++++++ src/PIL/ImageOps.py | 51 +++++++++++++++---------- 3 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 Tests/images/orientation_rectangle.jpg diff --git a/Tests/images/orientation_rectangle.jpg b/Tests/images/orientation_rectangle.jpg new file mode 100644 index 0000000000000000000000000000000000000000..85cfbd0a813a1b36d64318ead4f0fe5a3258a51f GIT binary patch literal 669 zcmex=wh=DOELf4NWZ*Q!{f5ODks= zS2uSLPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ&aO$5r%atTea6gLixw|g zx@`H1m8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds_3+W-Cr_U}fAR9w$4{TX zeEs(Q$Io9Ne=#yJL%anfAwEO%mmttzOe`$SEbJhEF*22dJTAz>s%Xe2([0-9])", ): - transposed_image.info["XML:com.adobe.xmp"] = re.sub( - pattern, "", transposed_image.info["XML:com.adobe.xmp"] + exif_image.info["XML:com.adobe.xmp"] = re.sub( + pattern, "", exif_image.info["XML:com.adobe.xmp"] ) + if inPlace: + return return transposed_image + if inPlace: + return return image.copy() From 099d696dc7d3c349265ae3cdbb5f949bca1e2866 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sat, 15 Apr 2023 18:24:19 +0800 Subject: [PATCH 242/512] Fix ImageGrab with wl-paste --- Tests/test_imagegrab.py | 19 +++++++++++++++++++ src/PIL/ImageGrab.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index fa88065f4..e7c2c6c9f 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -98,3 +98,22 @@ $ms = new-object System.IO.MemoryStream(, $bytes) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, "Tests/images/hopper.png") + + @pytest.mark.skipif( + ( + sys.platform != "linux" + or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + ), + reason="Linux with wl-clipboard only", + ) + @pytest.mark.parametrize( + "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] + ) + def test_grabclipboard_wl_clipboard(self, image_path): + with open(image_path, mode="rb") as raw_image: + try: + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) + except OSError as e: + pytest.skip(str(e)) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..175eb4671 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -135,6 +135,12 @@ def grabclipboard(): else: if shutil.which("wl-paste"): args = ["wl-paste"] + output = subprocess.check_output(["wl-paste", "-l"]).decode() + mime_types = output.splitlines() + for image_type in ["image/gif", "image/png"]: + if image_type in mime_types: + args.extend(["-t", image_type]) + break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From bcb8dfc2fa9011c99d6332d5b0aa00c82e1c6cdf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 22:30:18 +1000 Subject: [PATCH 243/512] Rearranged code --- src/PIL/ImageOps.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 460a21ce2..5dc34f442 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -622,9 +622,7 @@ def exif_transpose(image, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if inPlace: - return - return transposed_image - if inPlace: - return - return image.copy() + if not inPlace: + return transposed_image + elif not inPlace: + return image.copy() From 6acb381656626305fb2695ef955cfd8bec858570 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 17:23:08 +1000 Subject: [PATCH 244/512] Simplified NumPy install command --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 6a5f73857..2b597a945 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -99,7 +99,7 @@ jobs: - name: Install a different NumPy shell: dash.exe -l "{0}" run: | - python3 -m pip install -U 'numpy!=1.21.*' + python3 -m pip install -U numpy - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" From fe8599c5d64b2932b430a0178f17009855397fe6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 16 Apr 2023 14:04:39 +1000 Subject: [PATCH 245/512] Use ExifTags --- src/PIL/Image.py | 4 ++-- src/PIL/ImageOps.py | 8 ++++---- src/PIL/TiffImagePlugin.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a..34b8bbcbd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1418,12 +1418,12 @@ class Image: self._exif.load(exif_info) # XMP tags - if 0x0112 not in self._exif: + if ExifTags.Base.Orientation not in self._exif: xmp_tags = self.info.get("XML:com.adobe.xmp") if xmp_tags: match = re.search(r'tiff:Orientation(="|>)([0-9])', xmp_tags) if match: - self._exif[0x0112] = int(match[2]) + self._exif[ExifTags.Base.Orientation] = int(match[2]) return self._exif diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 5dc34f442..facc30ba0 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -21,7 +21,7 @@ import functools import operator import re -from . import Image, ImagePalette +from . import ExifTags, Image, ImagePalette # # helpers @@ -589,7 +589,7 @@ def exif_transpose(image, inPlace=False): image will be returned. """ image_exif = image.getexif() - orientation = image_exif.get(0x0112) + orientation = image_exif.get(ExifTags.Base.Orientation) method = { 2: Image.Transpose.FLIP_LEFT_RIGHT, 3: Image.Transpose.ROTATE_180, @@ -608,8 +608,8 @@ def exif_transpose(image, inPlace=False): exif_image = image if inPlace else transposed_image exif = exif_image.getexif() - if 0x0112 in exif: - del exif[0x0112] + if ExifTags.Base.Orientation in exif: + del exif[ExifTags.Base.Orientation] if "exif" in exif_image.info: exif_image.info["exif"] = exif.tobytes() elif "Raw profile type exif" in exif_image.info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..7f8449ea6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -49,7 +49,7 @@ from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational -from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags +from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 @@ -1183,7 +1183,7 @@ class TiffImageFile(ImageFile.ImageFile): :returns: Photoshop "Image Resource Blocks" in a dictionary. """ blocks = {} - val = self.tag_v2.get(0x8649) + val = self.tag_v2.get(ExifTags.Base.ImageResources) if val: while val[:4] == b"8BIM": id = i16(val[4:6]) @@ -1548,7 +1548,7 @@ class TiffImageFile(ImageFile.ImageFile): palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]] self.palette = ImagePalette.raw("RGB;L", b"".join(palette)) - self._tile_orientation = self.tag_v2.get(0x0112) + self._tile_orientation = self.tag_v2.get(ExifTags.Base.Orientation) # From 6d12581385688c3af964e6707a1b4ed2651d31a5 Mon Sep 17 00:00:00 2001 From: Carl Weaver Date: Sun, 16 Apr 2023 15:37:38 +0800 Subject: [PATCH 246/512] Update src/PIL/ImageGrab.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 175eb4671..6550a7706 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -136,10 +136,11 @@ def grabclipboard(): if shutil.which("wl-paste"): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - mime_types = output.splitlines() - for image_type in ["image/gif", "image/png"]: - if image_type in mime_types: - args.extend(["-t", image_type]) + clipboard_mimetypes = output.splitlines() + Image.preinit() + for mimetype in Image.MIME.values(): + if mimetype in clipboard_mimetypes: + args.extend(["-t", mimetype]) break elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] From 3d54b8e2b2419255a6b5a74dd0f2841ea4de7416 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Sun, 16 Apr 2023 15:41:14 +0800 Subject: [PATCH 247/512] Remove useless try catch block --- Tests/test_imagegrab.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index e7c2c6c9f..703472c4a 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -111,9 +111,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes) ) def test_grabclipboard_wl_clipboard(self, image_path): with open(image_path, mode="rb") as raw_image: - try: - subprocess.call(["wl-copy"], stdin=raw_image) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) - except OSError as e: - pytest.skip(str(e)) + subprocess.call(["wl-copy"], stdin=raw_image) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From 57bbe6df2c25a1a5155f4c25ae0bdf2f2769d035 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 15 Apr 2023 15:59:12 +1000 Subject: [PATCH 248/512] Remove use of deprecated "bpp" member --- src/libImaging/Jpeg2KEncode.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 8f6370061..0d7e896b7 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -281,7 +281,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { int ret = -1; unsigned prec = 8; - unsigned bpp = 8; unsigned _overflow_scale_factor; stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); @@ -313,7 +312,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { color_space = OPJ_CLRSPC_GRAY; pack = j2k_pack_i16; prec = 16; - bpp = 12; } else if (strcmp(im->mode, "LA") == 0) { components = 2; color_space = OPJ_CLRSPC_GRAY; @@ -342,7 +340,6 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { image_params[n].h = im->ysize; image_params[n].x0 = image_params[n].y0 = 0; image_params[n].prec = prec; - image_params[n].bpp = bpp; image_params[n].sgnd = context->sgnd == 0 ? 0 : 1; } From cc84ff5e7d721a1309ba8e6ebd80238f90af389d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Apr 2023 16:10:29 +1000 Subject: [PATCH 249/512] Note that open() seeks to the start of file objects --- src/PIL/Image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a..6dfde70a0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3156,7 +3156,8 @@ def open(fp, mode="r", formats=None): :param fp: A filename (string), pathlib.Path object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, - and be opened in binary mode. + and be opened in binary mode. The file object will also seek to zero + before reading. :param mode: The mode. If given, this argument must be "r". :param formats: A list or tuple of formats to attempt to load the file in. This can be used to restrict the set of formats checked. From aa2e662995eaf67e2f9f53d6817a173c12b45a19 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Mon, 17 Apr 2023 16:44:43 +0800 Subject: [PATCH 250/512] Add sway and wl-clipboard dependencies to GitHub CI workflow --- .ci/install.sh | 3 ++- .github/workflows/test.yml | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index 17c349ab1..d5cbd8248 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -22,7 +22,8 @@ set -e if [[ $(uname) != CYGWIN* ]]; then sudo apt-get -qq install libfreetype6-dev liblcms2-dev python3-tk\ ghostscript libffi-dev libjpeg-turbo-progs libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard fi python3 -m pip install --upgrade pip diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fced6113b..53b7ee688 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,7 +84,15 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh + export XDG_RUNTIME_DIR="/tmp/headless-sway" + export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" + export WLR_BACKENDS=headless + export WLR_LIBINPUT_NO_DEVICES=1 + mkdir "$XDG_RUNTIME_DIR" + xvfb-run -s '-screen 0 1024x768x24'\ + sway -V -d -c /dev/null& + export WAYLAND_DISPLAY=wayland-1 + .ci/test.sh else .ci/test.sh fi From 4e6f1f1ac60cb9e88c0605739468693e0e17e4e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 Apr 2023 19:08:59 +1000 Subject: [PATCH 251/512] Removed Fedora 36 --- .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 14592ea1d..cbe6c2ca3 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,7 +39,6 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, - fedora-36-amd64, fedora-37-amd64, gentoo, ubuntu-18.04-bionic-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 7088657f9..0ac5914da 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,8 +448,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ -| Fedora 36 | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | From 6ffa189d0156f52df422cd99fa3d28ebb0432107 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Apr 2023 09:16:01 +0000 Subject: [PATCH 252/512] Update cygwin/cygwin-install-action action to v4 --- .github/workflows/test-cygwin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 2b597a945..9a1e46705 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -39,7 +39,7 @@ jobs: uses: actions/checkout@v3 - name: Install Cygwin - uses: cygwin/cygwin-install-action@v3 + uses: cygwin/cygwin-install-action@v4 with: platform: x86_64 packages: > From b7585b0597855f15ccf998d84684d90488a1133a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:27:36 +1000 Subject: [PATCH 253/512] Removed unnecessary settings --- .github/workflows/test.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53b7ee688..afb8fb56c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,13 +84,7 @@ jobs: python3 -m pip install pytest-reverse fi if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then - export XDG_RUNTIME_DIR="/tmp/headless-sway" - export SWAYSOCK="$XDG_RUNTIME_DIR/sway.sock" - export WLR_BACKENDS=headless - export WLR_LIBINPUT_NO_DEVICES=1 - mkdir "$XDG_RUNTIME_DIR" - xvfb-run -s '-screen 0 1024x768x24'\ - sway -V -d -c /dev/null& + xvfb-run -s '-screen 0 1024x768x24' sway& export WAYLAND_DISPLAY=wayland-1 .ci/test.sh else From f15d7265f779c04d4e01b425f1e8b7211422a7dd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 18 Apr 2023 10:33:31 +1000 Subject: [PATCH 254/512] Call init() if mimetype is not found with preinit() --- Tests/test_imagegrab.py | 11 +++++------ src/PIL/ImageGrab.py | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 703472c4a..065c9c1b5 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -106,11 +106,10 @@ $ms = new-object System.IO.MemoryStream(, $bytes) ), reason="Linux with wl-clipboard only", ) - @pytest.mark.parametrize( - "image_path", ["Tests/images/hopper.gif", "Tests/images/hopper.png"] - ) - def test_grabclipboard_wl_clipboard(self, image_path): - with open(image_path, mode="rb") as raw_image: - subprocess.call(["wl-copy"], stdin=raw_image) + @pytest.mark.parametrize("ext", ("gif", "png", "ico")) + def test_grabclipboard_wl_clipboard(self, ext): + image_path = "Tests/images/hopper." + ext + with open(image_path, "rb") as fp: + subprocess.call(["wl-copy"], stdin=fp) im = ImageGrab.grabclipboard() assert_image_equal_tofile(im, image_path) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 6550a7706..55b50fb48 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -137,11 +137,19 @@ def grabclipboard(): args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() clipboard_mimetypes = output.splitlines() + + def find_mimetype(): + for mime in Image.MIME.values(): + if mime in clipboard_mimetypes: + return mime + Image.preinit() - for mimetype in Image.MIME.values(): - if mimetype in clipboard_mimetypes: - args.extend(["-t", mimetype]) - break + mimetype = find_mimetype() + if not mimetype: + Image.init() + mimetype = find_mimetype() + if mimetype: + args.extend(["-t", mimetype]) elif shutil.which("xclip"): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: From 75c4acf02c6128f1fe03a14c202a6bcf57eaa31f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 19 Apr 2023 06:46:58 -0600 Subject: [PATCH 255/512] Fix typo --- docs/reference/Image.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 35a4c2110..41d3b8fce 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -439,7 +439,7 @@ Used to specify the dithering method to use for the Palettes ^^^^^^^^ -Used to specify the pallete to use for the :meth:`~Image.convert` method. +Used to specify the palette to use for the :meth:`~Image.convert` method. .. autoclass:: Palette :members: From d2256338b82730fef647a3cd8b65bc9040a7d73e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 20 Apr 2023 23:15:20 +1000 Subject: [PATCH 256/512] Use later value for duplicate xref entries --- Tests/images/duplicate_xref_entry.pdf | Bin 0 -> 3326 bytes Tests/test_pdfparser.py | 6 ++++++ src/PIL/PdfParser.py | 9 +++------ 3 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 Tests/images/duplicate_xref_entry.pdf diff --git a/Tests/images/duplicate_xref_entry.pdf b/Tests/images/duplicate_xref_entry.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f57a57d61c6ad0649448af316f689db4e7f1322e GIT binary patch literal 3326 zcmeH}S#Z-v7{?{cr{WAa2{dFvvpL)(9cpQ1`C?K!#KsU>hQvt=rPBu=n?xDMN@V3? zrVr4YH+r{EK<~cMmR?V#rRN(xfxeX9?GyB-54hh-K4N>ADRh|WbXZv$>)UU4zfY_G z_mg)x7QYVrWZsL?8cFITgHlUqSjlG91%yQ(Ju+loMBs-qnleu`UPdBPQ&R&2yfC&j zLy!dCA+!3)F536e(v=uhw)HjrEf+<1Cht5G`bra+-sVeSG25c>$rMtTYEd|@%5sv zb=~dleWCt!B9>*jqc?JWSQ_y8WrcSs#sUif`UBv~_gs z=392`ymePPL&hg2m8rZwH@~pBwENDx?!M>V`|jWOz=IDx{K%t^J^sX@C!c!ynP;DS z{)HD`dilsJufF#B8*jaR?45Vtd;fzEKl=ESPe1$o#K}{qzxw){Z@>HghaZ1B`|~fq z{`UJHf1dk`=EX9cnHF5l#A@>LKwcKBm9si%UaVySPR?Vsbz*zd#t}zywz*5%<7^q+ zfAH{8SGPZLW>rc%&adu~PkYbO)QrsjSz>!HDYJ57mApuq>@K|!@nqas$i;ijjE_d?$oguT~S4{zapYpI=RtG zD^+gN(?~Fbi>YXYuTMN!nR-movjp{>FEBbom^`ERL#(ubDQYeTWeoMj)=Q$~7iCGr zQb3eLyTD_cnz>+Sxn3=5WSkdKh&SV;R}>5c{6RF1N;VvTSd5-r*%pz*wKj~!dOtA$ z8(1?|j6`Z}7)O?k)wQIOh1yHEIiadLqD43Xl~CDDAXH%}H?AW3e2hKK>q$*F^1xCg z!h5}&w#U7fQ0ei1>{Y|1x3;JROyAZjw$z}gZa^JVs$e9QVPs^UK(M3LObXR5c!fz9 zM%5K2RkQYg%p@w5f6XK+u8Uz3)J$3f&5zJ3Ce`YmdR`_d;bp+P{8Yjv0*Rtjp^^mO zz6Mh01;_vmQ(DPLs@Q?kB|H6Nwu&tXpTnjN$OAh;8!FQchh?a>)ix>UuOpchia?32 z_QN4*8Oe-J5r;x%Jj>Sz1nc~yaCq4obrduJP{JUV=_e4GM+WpvBpSO@!$c_(4wy{* E8-kd> ]" assert pdf_repr(PdfBinary(b"\x90\x1F\xA0")) == b"<901FA0>" + + +def test_duplicate_xref_entry(): + pdf = PdfParser("Tests/images/duplicate_xref_entry.pdf") + assert pdf.xref_table.existing_entries[6][0] == 1197 + pdf.close() diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 1b3cb52a2..dc1012f54 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -957,14 +957,11 @@ class PdfParser: check_format_condition(m, "xref entry not found") offset = m.end() is_free = m.group(3) == b"f" - generation = int(m.group(2)) if not is_free: + generation = int(m.group(2)) new_entry = (int(m.group(1)), generation) - check_format_condition( - i not in self.xref_table or self.xref_table[i] == new_entry, - "xref entry duplicated (and not identical)", - ) - self.xref_table[i] = new_entry + if i not in self.xref_table: + self.xref_table[i] = new_entry return offset def read_indirect(self, ref, max_nesting=-1): From 895f5a4ffc70fafaae2d16e6618c373e1689186d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Apr 2023 09:04:46 +1000 Subject: [PATCH 257/512] Updated macOS tested Pillow versions [ci skip] --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 0ac5914da..8798c0791 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -490,7 +490,7 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.4.0 |arm | +| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ From b10379b3c14eba9d32a5e81d5242c7c5857a32cc Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Fri, 21 Apr 2023 17:42:45 +0300 Subject: [PATCH 258/512] Load image before deepcopy(__getstate__) Signed-off-by: bigcat88 --- Tests/test_numpy.py | 9 +++++++++ src/PIL/Image.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 147f94a71..6dc53d1e8 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,4 +1,5 @@ import warnings +from copy import deepcopy import pytest @@ -226,6 +227,14 @@ def test_load_first(): assert a.shape == (88, 590) +@skip_unless_feature("libtiff") +def test_load_first_deepcopy(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + im_deepcopy = deepcopy(im) + a = numpy.array(im_deepcopy) + assert a.shape == (88, 590) + + def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 5a43f6c4a..bee9e23d0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -672,7 +672,8 @@ class Image: return new def __getstate__(self): - return [self.info, self.mode, self.size, self.getpalette(), self.tobytes()] + im_data = self.tobytes() # load image first + return [self.info, self.mode, self.size, self.getpalette(), im_data] def __setstate__(self, state): Image.__init__(self) From 91b69857c78cb75bb096306a6460c27e70a55c38 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 10:13:56 +1000 Subject: [PATCH 259/512] Removed duplicate code --- src/PIL/ImageCms.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 31b0e5a5e..38cbab19c 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -185,12 +185,8 @@ class ImageCmsProfile: def _set(self, profile, filename=None): self.profile = profile self.filename = filename - if profile: - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info - else: - self.product_name = None - self.product_info = None + self.product_name = None # profile.product_name + self.product_info = None # profile.product_info def tobytes(self): """ From b62287da3a529bc8e47ab330a81dfd1d8959cef8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 11:18:56 +1000 Subject: [PATCH 260/512] Moved test to test_image_copy --- Tests/test_image_copy.py | 9 ++++++++- Tests/test_numpy.py | 9 --------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Tests/test_image_copy.py b/Tests/test_image_copy.py index 591832147..cd602fc76 100644 --- a/Tests/test_image_copy.py +++ b/Tests/test_image_copy.py @@ -4,7 +4,7 @@ import pytest from PIL import Image -from .helper import hopper +from .helper import hopper, skip_unless_feature @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) @@ -42,3 +42,10 @@ def test_copy_zero(): out = im.copy() assert out.mode == im.mode assert out.size == im.size + + +@skip_unless_feature("libtiff") +def test_deepcopy(): + with Image.open("Tests/images/g4_orientation_5.tif") as im: + out = copy.deepcopy(im) + assert out.size == (590, 88) diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 6dc53d1e8..147f94a71 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -1,5 +1,4 @@ import warnings -from copy import deepcopy import pytest @@ -227,14 +226,6 @@ def test_load_first(): assert a.shape == (88, 590) -@skip_unless_feature("libtiff") -def test_load_first_deepcopy(): - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im_deepcopy = deepcopy(im) - a = numpy.array(im_deepcopy) - assert a.shape == (88, 590) - - def test_bool(): # https://github.com/python-pillow/Pillow/issues/2044 a = numpy.zeros((10, 2), dtype=bool) From 81a756e93b102b20a8c4599d4b5b3235109ef5ab Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 13:45:18 +1000 Subject: [PATCH 261/512] Support float font sizes --- Tests/test_imagefont.py | 10 ++++++++++ src/_imagingft.c | 14 +++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 623365d53..7ea485a55 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -191,6 +191,16 @@ def test_getlength( assert length == length_raqm +def test_float_size(): + lengths = [] + for size in (48, 48.5, 49): + f = ImageFont.truetype( + "Tests/fonts/NotoSans-Regular.ttf", size, layout_engine=layout_engine + ) + lengths.append(f.getlength("text")) + assert lengths[0] != lengths[1] != lengths[2] + + def test_render_multiline(font): im = Image.new(mode="RGB", size=(300, 100)) draw = ImageDraw.Draw(im) diff --git a/src/_imagingft.c b/src/_imagingft.c index 19785a47f..78e3f7f10 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -116,7 +116,9 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { int error = 0; char *filename = NULL; - Py_ssize_t size; + float size; + FT_Size_RequestRec req; + FT_Long width; Py_ssize_t index = 0; Py_ssize_t layout_engine = 0; unsigned char *encoding; @@ -133,7 +135,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { if (!PyArg_ParseTupleAndKeywords( args, kw, - "etn|nsy#n", + "etf|nsy#n", kwlist, Py_FileSystemDefaultEncoding, &filename, @@ -179,7 +181,13 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { } if (!error) { - error = FT_Set_Pixel_Sizes(self->face, 0, size); + width = size * 64; + req.type = FT_SIZE_REQUEST_TYPE_NOMINAL; + req.width = width; + req.height = width; + req.horiResolution = 0; + req.vertResolution = 0; + error = FT_Request_Size(self->face, &req); } if (!error && encoding && strlen((char *)encoding) == 4) { From d0b41da094c48077c3511988e98c168bd45f84dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 21:22:01 +1000 Subject: [PATCH 262/512] Support I mode for BuiltinFilter --- Tests/images/hopper_emboss_I.png | Bin 0 -> 13273 bytes Tests/images/hopper_emboss_more_I.png | Bin 0 -> 14624 bytes Tests/test_image_filter.py | 43 +++++---- src/libImaging/Filter.c | 133 ++++++++++++++++++-------- 4 files changed, 120 insertions(+), 56 deletions(-) create mode 100644 Tests/images/hopper_emboss_I.png create mode 100644 Tests/images/hopper_emboss_more_I.png diff --git a/Tests/images/hopper_emboss_I.png b/Tests/images/hopper_emboss_I.png new file mode 100644 index 0000000000000000000000000000000000000000..f4dab388fe5c2dce3771133525df2c09b0fada45 GIT binary patch literal 13273 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 WC}ig2^aR-e0000= pow(2, 31) - 1) { + return pow(2, 31) - 1; + } + return (INT32)in; +} + Imaging ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { Imaging imOut; @@ -96,8 +107,8 @@ ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { void ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x3(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d]) * (kernel)[0] + _i2f((UINT8)in0[x]) * (kernel)[1] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[2]) + (_i2f(in0[x - d]) * (kernel)[0] + _i2f(in0[x]) * (kernel)[1] + \ + _i2f(in0[x + d]) * (kernel)[2]) int x = 0, y = 0; @@ -105,21 +116,40 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 1; y < im->ysize - 1; y++) { - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 1; y < im->ysize - 1; y++) { + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - for (x = 1; x < im->xsize - 1; x++) { - float ss = offset; - ss += KERNEL1x3(in1, x, &kernel[0], 1); - ss += KERNEL1x3(in0, x, &kernel[3], 1); - ss += KERNEL1x3(in_1, x, &kernel[6], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip32(ss); + } + out[x] = in0[x]; + } + } else { + for (y = 1; y < im->ysize - 1; y++) { + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + for (x = 1; x < im->xsize - 1; x++) { + float ss = offset; + ss += KERNEL1x3(in1, x, &kernel[0], 1); + ss += KERNEL1x3(in0, x, &kernel[3], 1); + ss += KERNEL1x3(in_1, x, &kernel[6], 1); + out[x] = clip8(ss); + } + out[x] = in0[x]; } - out[x] = in0[x]; } } else { // Add one time for rounding @@ -195,10 +225,10 @@ ImagingFilter3x3(Imaging imOut, Imaging im, const float *kernel, float offset) { void ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { #define KERNEL1x5(in0, x, kernel, d) \ - (_i2f((UINT8)in0[x - d - d]) * (kernel)[0] + \ - _i2f((UINT8)in0[x - d]) * (kernel)[1] + _i2f((UINT8)in0[x]) * (kernel)[2] + \ - _i2f((UINT8)in0[x + d]) * (kernel)[3] + \ - _i2f((UINT8)in0[x + d + d]) * (kernel)[4]) + (_i2f(in0[x - d - d]) * (kernel)[0] + \ + _i2f(in0[x - d]) * (kernel)[1] + _i2f(in0[x]) * (kernel)[2] + \ + _i2f(in0[x + d]) * (kernel)[3] + \ + _i2f(in0[x + d + d]) * (kernel)[4]) int x = 0, y = 0; @@ -207,27 +237,52 @@ ImagingFilter5x5(Imaging imOut, Imaging im, const float *kernel, float offset) { if (im->bands == 1) { // Add one time for rounding offset += 0.5; - for (y = 2; y < im->ysize - 2; y++) { - UINT8 *in_2 = (UINT8 *)im->image[y - 2]; - UINT8 *in_1 = (UINT8 *)im->image[y - 1]; - UINT8 *in0 = (UINT8 *)im->image[y]; - UINT8 *in1 = (UINT8 *)im->image[y + 1]; - UINT8 *in2 = (UINT8 *)im->image[y + 2]; - UINT8 *out = (UINT8 *)imOut->image[y]; + if (im->type == IMAGING_TYPE_INT32) { + for (y = 2; y < im->ysize - 2; y++) { + INT32 *in_2 = (INT32 *)im->image[y - 2]; + INT32 *in_1 = (INT32 *)im->image[y - 1]; + INT32 *in0 = (INT32 *)im->image[y]; + INT32 *in1 = (INT32 *)im->image[y + 1]; + INT32 *in2 = (INT32 *)im->image[y + 2]; + INT32 *out = (INT32 *)imOut->image[y]; - out[0] = in0[0]; - out[1] = in0[1]; - for (x = 2; x < im->xsize - 2; x++) { - float ss = offset; - ss += KERNEL1x5(in2, x, &kernel[0], 1); - ss += KERNEL1x5(in1, x, &kernel[5], 1); - ss += KERNEL1x5(in0, x, &kernel[10], 1); - ss += KERNEL1x5(in_1, x, &kernel[15], 1); - ss += KERNEL1x5(in_2, x, &kernel[20], 1); - out[x] = clip8(ss); + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip32(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; + } + } else { + for (y = 2; y < im->ysize - 2; y++) { + UINT8 *in_2 = (UINT8 *)im->image[y - 2]; + UINT8 *in_1 = (UINT8 *)im->image[y - 1]; + UINT8 *in0 = (UINT8 *)im->image[y]; + UINT8 *in1 = (UINT8 *)im->image[y + 1]; + UINT8 *in2 = (UINT8 *)im->image[y + 2]; + UINT8 *out = (UINT8 *)imOut->image[y]; + + out[0] = in0[0]; + out[1] = in0[1]; + for (x = 2; x < im->xsize - 2; x++) { + float ss = offset; + ss += KERNEL1x5(in2, x, &kernel[0], 1); + ss += KERNEL1x5(in1, x, &kernel[5], 1); + ss += KERNEL1x5(in0, x, &kernel[10], 1); + ss += KERNEL1x5(in_1, x, &kernel[15], 1); + ss += KERNEL1x5(in_2, x, &kernel[20], 1); + out[x] = clip8(ss); + } + out[x + 0] = in0[x + 0]; + out[x + 1] = in0[x + 1]; } - out[x + 0] = in0[x + 0]; - out[x + 1] = in0[x + 1]; } } else { // Add one time for rounding @@ -327,7 +382,7 @@ ImagingFilter(Imaging im, int xsize, int ysize, const FLOAT32 *kernel, FLOAT32 o Imaging imOut; ImagingSectionCookie cookie; - if (!im || im->type != IMAGING_TYPE_UINT8) { + if (im->type != IMAGING_TYPE_UINT8 && im->type != IMAGING_TYPE_INT32) { return (Imaging)ImagingError_ModeError(); } From f5c1f7a2c27c810d3d95dc659c5c24c455ca6f74 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Apr 2023 21:47:36 +1000 Subject: [PATCH 263/512] Added Fedora 38 --- .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 cbe6c2ca3..84c46b1d4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -40,6 +40,7 @@ jobs: centos-stream-9-amd64, debian-11-bullseye-x86, fedora-37-amd64, + fedora-38-amd64, gentoo, ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index 8798c0791..21dcd0227 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -450,6 +450,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 38 | 3.11 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9, 3.10, 3.11, | x86-64 | From e3cb4bb8e00fcaf4c3e0783f7c02e51372595659 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Apr 2023 07:48:53 +1000 Subject: [PATCH 264/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cd0b95085..b82333af7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Load before getting size in __getstate__ #7105 + [bigcat88, radarhere] + - Fixed type handling for include and lib directories #7069 [adisbladis, radarhere] From ab3d0c071e60da904062d22f6b9a73e8fc6cdcb9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Apr 2023 21:03:27 +1000 Subject: [PATCH 265/512] Raise error from stderr of Linux grabclipboard command --- src/PIL/ImageGrab.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 982f77f20..2592ba2df 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -141,8 +141,11 @@ def grabclipboard(): msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) fh, filepath = tempfile.mkstemp() - subprocess.call(args, stdout=fh) + err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr os.close(fh) + if err: + msg = f"{args[0]} error: {err.strip().decode()}" + raise ChildProcessError(msg) im = Image.open(filepath) im.load() os.unlink(filepath) From 99a474a9e63d5bf2ea2654bb8102731db90ff8c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 25 Apr 2023 23:55:29 +1000 Subject: [PATCH 266/512] Removed Ubuntu 18.04 --- .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 84c46b1d4..4f01abe44 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -42,7 +42,6 @@ jobs: fedora-37-amd64, fedora-38-amd64, gentoo, - ubuntu-18.04-bionic-amd64, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, ] diff --git a/docs/installation.rst b/docs/installation.rst index 21dcd0227..a254ec8c2 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -457,8 +457,6 @@ These platforms are built and tested for every change. | macOS 12 Monterey | 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.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.8, 3.9, 3.10, 3.11, | x86-64 | From d0e9a13c0c8ee0c77559e604c8758720991d930a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 26 Apr 2023 17:08:06 +1000 Subject: [PATCH 267/512] Build all readthedocs formats --- .readthedocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 98d9e4425..ec3300dd1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,7 @@ version: 2 +formats: all + build: os: ubuntu-22.04 tools: From 0c8db130afe5343358b30fadffa6e59d2c877083 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 27 Apr 2023 13:31:14 +1000 Subject: [PATCH 268/512] Updated harfbuzz to 7.2.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 3f639454b..9b5fc5d18 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -337,9 +337,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.1.0.zip", - "filename": "harfbuzz-7.1.0.zip", - "dir": "harfbuzz-7.1.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", + "filename": "harfbuzz-7.2.0.zip", + "dir": "harfbuzz-7.2.0", "license": "COPYING", "build": [ *cmds_cmake( From ebd3c47425fe5535f8d1c8eab15bbf1e52899939 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Apr 2023 15:02:11 +1000 Subject: [PATCH 269/512] When saving, allow alpha differences to indicate different frames --- Tests/test_file_apng.py | 14 ++++++++++++++ src/PIL/PngImagePlugin.py | 4 ++-- src/_imaging.c | 12 +++++++++--- src/libImaging/GetBBox.c | 7 ++++--- src/libImaging/Imaging.h | 2 +- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb..cf1312b77 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -374,6 +374,20 @@ def test_apng_save(tmp_path): assert im.getpixel((64, 32)) == (0, 255, 0, 255) +def test_apng_save_alpha(tmp_path): + test_file = str(tmp_path / "temp.png") + + im = Image.new("RGBA", (1, 1), (255, 0, 0, 255)) + im2 = Image.new("RGBA", (1, 1), (255, 0, 0, 127)) + im.save(test_file, save_all=True, append_images=[im2]) + + with Image.open(test_file) as reloaded: + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 255) + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == (255, 0, 0, 127) + + def test_apng_save_split_fdat(tmp_path): # test to make sure we do not generate sequence errors when writing # frames with image data spanning multiple fdAT chunks (in this case diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b267..7afd6a2a6 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1138,9 +1138,9 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) else: base_im = previous["im"] delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - bbox = delta.getbbox() + bbox = delta.im.getbbox(False) if ( not bbox and prev_disposal == encoderinfo.get("disposal") diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2..62e51da26 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2160,9 +2160,15 @@ _isblock(ImagingObject *self) { } static PyObject * -_getbbox(ImagingObject *self) { +_getbbox(ImagingObject *self, PyObject *args) { int bbox[4]; - if (!ImagingGetBBox(self->image, bbox)) { + + int consider_alpha = 1; + if (!PyArg_ParseTuple(args, "|i", &consider_alpha)) { + return NULL; + } + + if (!ImagingGetBBox(self->image, bbox, consider_alpha)) { Py_INCREF(Py_None); return Py_None; } @@ -3574,7 +3580,7 @@ static struct PyMethodDef methods[] = { {"isblock", (PyCFunction)_isblock, METH_NOARGS}, - {"getbbox", (PyCFunction)_getbbox, METH_NOARGS}, + {"getbbox", (PyCFunction)_getbbox, METH_VARARGS}, {"getcolors", (PyCFunction)_getcolors, METH_VARARGS}, {"getextrema", (PyCFunction)_getextrema, METH_NOARGS}, {"getprojection", (PyCFunction)_getprojection, METH_NOARGS}, diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index e73153600..c1570cd3e 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -19,7 +19,7 @@ #include "Imaging.h" int -ImagingGetBBox(Imaging im, int bbox[4]) { +ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -58,10 +58,11 @@ ImagingGetBBox(Imaging im, int bbox[4]) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if ( + } else if (consider_alpha && ( strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || - strcmp(im->mode, "PA") == 0) { + strcmp(im->mode, "PA") == 0 + )) { #ifdef WORDS_BIGENDIAN mask = 0x000000ff; #else diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded1852..2563a0c62 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -317,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int -ImagingGetBBox(Imaging im, int bbox[4]); +ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha); typedef struct { int x, y; INT32 count; From 96bdbc4afe8ea9c6f113c2676368b2a11bc9fefb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Apr 2023 19:11:02 +1000 Subject: [PATCH 270/512] Renamed variable --- src/_imaging.c | 6 +++--- src/libImaging/GetBBox.c | 4 ++-- src/libImaging/Imaging.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 62e51da26..87f5b6705 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -2163,12 +2163,12 @@ static PyObject * _getbbox(ImagingObject *self, PyObject *args) { int bbox[4]; - int consider_alpha = 1; - if (!PyArg_ParseTuple(args, "|i", &consider_alpha)) { + int alpha_only = 1; + if (!PyArg_ParseTuple(args, "|i", &alpha_only)) { return NULL; } - if (!ImagingGetBBox(self->image, bbox, consider_alpha)) { + if (!ImagingGetBBox(self->image, bbox, alpha_only)) { Py_INCREF(Py_None); return Py_None; } diff --git a/src/libImaging/GetBBox.c b/src/libImaging/GetBBox.c index c1570cd3e..86c687ca0 100644 --- a/src/libImaging/GetBBox.c +++ b/src/libImaging/GetBBox.c @@ -19,7 +19,7 @@ #include "Imaging.h" int -ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only) { /* Get the bounding box for any non-zero data in the image.*/ int x, y; @@ -58,7 +58,7 @@ ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha) { INT32 mask = 0xffffffff; if (im->bands == 3) { ((UINT8 *)&mask)[3] = 0; - } else if (consider_alpha && ( + } else if (alpha_only && ( strcmp(im->mode, "RGBa") == 0 || strcmp(im->mode, "RGBA") == 0 || strcmp(im->mode, "La") == 0 || strcmp(im->mode, "LA") == 0 || strcmp(im->mode, "PA") == 0 diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 2563a0c62..42420887d 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -317,7 +317,7 @@ ImagingMerge(const char *mode, Imaging bands[4]); extern int ImagingSplit(Imaging im, Imaging bands[4]); extern int -ImagingGetBBox(Imaging im, int bbox[4], int consider_alpha); +ImagingGetBBox(Imaging im, int bbox[4], int alpha_only); typedef struct { int x, y; INT32 count; From b62c3baeeedb642b8eaf4dc16245fb54bcb92dfd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Apr 2023 06:32:27 +1000 Subject: [PATCH 271/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b82333af7..7b13900a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Support float font sizes #7107 + [radarhere] + +- Use later value for duplicate xref entries in PdfParser #7102 + [radarhere] + - Load before getting size in __getstate__ #7105 [bigcat88, radarhere] From ff003bfbcc9cfd7d281030f836616c0cdd59cfa6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 30 Apr 2023 14:49:40 +1000 Subject: [PATCH 272/512] Added unpacker from I;16B to I;16 --- Tests/test_lib_pack.py | 1 + src/libImaging/Unpack.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index de3e7d156..f7812f62b 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -757,6 +757,7 @@ class TestLibUnpack: def test_I16(self): self.assert_unpack("I;16", "I;16", 2, 0x0201, 0x0403, 0x0605) + self.assert_unpack("I;16", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16B", "I;16B", 2, 0x0102, 0x0304, 0x0506) self.assert_unpack("I;16L", "I;16L", 2, 0x0201, 0x0403, 0x0605) self.assert_unpack("I;16", "I;12", 2, 0x0010, 0x0203, 0x0040) diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index 7eeadf944..a0fa22c7d 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1149,6 +1149,16 @@ unpackI16N_I16(UINT8 *out, const UINT8 *in, int pixels) { } } static void +unpackI16B_I16(UINT8 *out, const UINT8 *in, int pixels) { + int i; + for (i = 0; i < pixels; i++) { + out[0] = in[1]; + out[1] = in[0]; + in += 2; + out += 2; + } +} +static void unpackI16R_I16(UINT8 *out, const UINT8 *in, int pixels) { int i; for (i = 0; i < pixels; i++) { @@ -1764,6 +1774,7 @@ static struct { {"I;16L", "I;16L", 16, copy2}, {"I;16N", "I;16N", 16, copy2}, + {"I;16", "I;16B", 16, unpackI16B_I16}, {"I;16", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16L", "I;16N", 16, unpackI16N_I16}, // LibTiff native->image endian. {"I;16B", "I;16N", 16, unpackI16N_I16B}, From b7d19e83d00b08c643c69bca6ea3f6e603c72a98 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:15:29 +0000 Subject: [PATCH 273/512] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/tox-dev/tox-ini-fmt: 1.0.0 → 1.3.0](https://github.com/tox-dev/tox-ini-fmt/compare/1.0.0...1.3.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 c3b6dc0a6..4882a317f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -57,7 +57,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.0.0 + rev: 1.3.0 hooks: - id: tox-ini-fmt From a766fa4cd1c749ca27375079ef92a57dc538c905 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:16:00 +0000 Subject: [PATCH 274/512] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tox.ini | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index d7948ef6d..458a00107 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] -minversion = 1.9 -envlist = +requires = + tox>=4.2 +env_list = lint py{py3, 311, 310, 39, 38} @@ -23,7 +24,7 @@ skip_install = true deps = check-manifest pre-commit -passenv = +pass_env = PRE_COMMIT_COLOR commands = pre-commit run --all-files --show-diff-on-failure From 15ef533df9847e556eca0eaa50b7738bb71b8c34 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 2 May 2023 08:41:18 +1000 Subject: [PATCH 275/512] Added alpha_only argument to getbbox() --- Tests/test_image_getbbox.py | 15 +++++++++++++++ src/PIL/Image.py | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index af69ed57a..eec978210 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -1,3 +1,5 @@ +import pytest + from PIL import Image from .helper import hopper @@ -38,3 +40,16 @@ def test_bbox(): for color in ((0, 0), (127, 0), (255, 0)): im = Image.new(mode, (100, 100), color) check(im, (255, 255)) + + +@pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) +def test_bbox_alpha_only_false(mode): + im = Image.new(mode, (100, 100)) + assert im.getbbox(False) is None + + fill_color = [1] * Image.getmodebands(mode) + fill_color[-1] = 0 + im.paste(tuple(fill_color), (25, 25, 75, 75)) + assert im.getbbox(False) == (25, 25, 75, 75) + + assert im.getbbox() is None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bee9e23d0..f5d120671 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1279,11 +1279,14 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self): + def getbbox(self, alpha_only=True): """ Calculates the bounding box of the non-zero regions in the image. + :param alpha_only: Optional flag, defaulting to true. + If true and the image has an alpha channel, trim transparent pixels. + Otherwise, trim pixels when all channels are zero. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See :ref:`coordinate-system`. If the image is completely empty, this @@ -1292,7 +1295,7 @@ class Image: """ self.load() - return self.im.getbbox() + return self.im.getbbox(alpha_only) def getcolors(self, maxcolors=256): """ From d9921f697aed24bd21b7c090bb6edd55acc8997f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 May 2023 08:29:20 +1000 Subject: [PATCH 276/512] Use stdlib for setuptools on MinGW --- .github/workflows/test-mingw.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index ddfafc9d7..a109ec0d8 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,7 +80,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . - name: Test Pillow run: | From db7326674e5261e19fa35f3a32efaf6dea48a5cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 1 May 2023 13:03:31 +1000 Subject: [PATCH 277/512] Updated libimagequant to 4.2.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 362ad95a2..fd6000ee1 100755 --- a/depends/install_imagequant.sh +++ b/depends/install_imagequant.sh @@ -1,7 +1,7 @@ #!/bin/bash # install libimagequant -archive=libimagequant-4.1.1 +archive=libimagequant-4.2.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 a254ec8c2..ad27b67ee 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -181,7 +181,7 @@ Many of Pillow's features require external libraries: * **libimagequant** provides improved color quantization - * Pillow has been tested with libimagequant **2.6-4.1.1** + * Pillow has been tested with libimagequant **2.6-4.2** * Libimagequant is licensed GPLv3, which is more restrictive than the Pillow license, therefore we will not be distributing binaries with libimagequant support enabled. From 3fc446c2770b091b07d3b6cff63e87385c19e7fc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 3 May 2023 22:54:18 +1000 Subject: [PATCH 278/512] Added width argument to regular_polygon --- Tests/images/imagedraw_triangle_width.png | Bin 0 -> 499 bytes Tests/test_imagedraw.py | 20 ++++++++++---------- docs/reference/ImageDraw.rst | 3 ++- src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 Tests/images/imagedraw_triangle_width.png diff --git a/Tests/images/imagedraw_triangle_width.png b/Tests/images/imagedraw_triangle_width.png new file mode 100644 index 0000000000000000000000000000000000000000..3d35326e73b92ffb24b9d64cc771662eee87000c GIT binary patch literal 499 zcmeAS@N?(olHy`uVBq!ia0vp^DImD;uumf=k2YHeTNK0SOcs7 z|DV6msp$lZm}+J5+nH&zb(QAFEMItk*2G_{>lmti1tOXb2(e64*uc`L%Aw$qU_*5&+_>uupeX5C~oixu{%*F`cV5M`^+^t6UwtW;ym>{J}i7cmHX4mfDJwq z^7*X=RE1MQ*4H&8pJJBW`?dD-_sjpAPcv~^--$oPB)K)#^@mrH!?$&-I!`hF^gpP5 zlEE@dmQkZ-LOj3W7puq3J-@zfU|z3hc0x)1)AJh&?jqZS^iDLr&gkVSN^#UqY|`1) zcdbJqeeX_1?T3v$R>iUgB4Q_QhKC;5dYoye(vPc086$r<|C3>U^2%Ifj}Y656|C1U zN--{ZwW?{|61kQzLoZ#AukA7wy5BZkXDqONb1i$vo=pi?8!lYG7o7YVvOvzStWc37?}*7u6{1-oD!M<@Fc=% literal 0 HcmV?d00001 diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7ffd7969d..406f44c06 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1347,20 +1347,20 @@ def test_same_color_outline(): @pytest.mark.parametrize( - "n_sides, rotation, polygon_name", - [(4, 0, "square"), (8, 0, "regular_octagon"), (4, 45, "square")], + "n_sides, polygon_name, args", + [ + (4, "square", {}), + (8, "regular_octagon", {}), + (4, "square_rotate_45", {"rotation": 45}), + (3, "triangle_width", {"width": 5, "outline": "yellow"}), + ], ) -def test_draw_regular_polygon(n_sides, rotation, polygon_name): +def test_draw_regular_polygon(n_sides, polygon_name, args): im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) - filename_base = f"Tests/images/imagedraw_{polygon_name}" - filename = ( - f"{filename_base}.png" - if rotation == 0 - else f"{filename_base}_rotate_{rotation}.png" - ) + filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) bounding_circle = ((W // 2, H // 2), 25) - draw.regular_polygon(bounding_circle, n_sides, rotation=rotation, fill="red") + draw.regular_polygon(bounding_circle, n_sides, fill="red", **args) assert_image_equal_tofile(im, filename) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index aec7a3ef8..29115120c 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -296,7 +296,7 @@ Methods :param width: The line width, in pixels. -.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None) +.. py:method:: ImageDraw.regular_polygon(bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1) Draws a regular polygon inscribed in ``bounding_circle``, with ``n_sides``, and rotation of ``rotation`` degrees. @@ -311,6 +311,7 @@ Methods (e.g. ``rotation=90``, applies a 90 degree rotation). :param fill: Color to use for the fill. :param outline: Color to use for the outline. + :param width: The line width, in pixels. .. py:method:: ImageDraw.rectangle(xy, fill=None, outline=None, width=1) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e9ccf8041..1e4eeab25 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -279,11 +279,11 @@ class ImageDraw: self.im.paste(im.im, (0, 0) + im.size, mask.im) def regular_polygon( - self, bounding_circle, n_sides, rotation=0, fill=None, outline=None + self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 ): """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) - self.polygon(xy, fill, outline) + self.polygon(xy, fill, outline, width) def rectangle(self, xy, fill=None, outline=None, width=1): """Draw a rectangle.""" From a4986ba9866797648b602ce62fee393f43b522df Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 07:54:30 +1000 Subject: [PATCH 279/512] Support reading signed 8-bit TIFF images --- Tests/images/8bit.s.tif | Bin 0 -> 16518 bytes Tests/test_file_tiff.py | 6 ++++++ src/PIL/TiffImagePlugin.py | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 Tests/images/8bit.s.tif diff --git a/Tests/images/8bit.s.tif b/Tests/images/8bit.s.tif new file mode 100644 index 0000000000000000000000000000000000000000..043cba6af8bbfe6d7386ac7ca247c5235c0dc387 GIT binary patch literal 16518 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 z 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 TR=N?=Kr4|!DMe+Lt#1AQlp!)* literal 0 HcmV?d00001 diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 97a02ac96..30c6303a2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -198,6 +198,12 @@ class TestFileTiff: with pytest.raises(OSError): im.save(outfile) + def test_8bit_s(self): + with Image.open("Tests/images/8bit.s.tif") as im: + im.load() + assert im.mode == "L" + assert im.getpixel((50, 50)) == 184 + def test_little_endian(self): with Image.open("Tests/images/16bit.cropped.tif") as im: assert im.getpixel((0, 0)) == 480 diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d4d0910a..1ca1b6ea9 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -170,6 +170,8 @@ OPEN_INFO = { (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"), (II, 1, (1,), 1, (8,), ()): ("L", "L"), (MM, 1, (1,), 1, (8,), ()): ("L", "L"), + (II, 1, (2,), 1, (8,), ()): ("L", "L"), + (MM, 1, (2,), 1, (8,), ()): ("L", "L"), (II, 1, (1,), 2, (8,), ()): ("L", "L;R"), (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"), (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"), From 9154a6b22d2915d4ca689054a239ceeed078ef2d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:01:48 +1000 Subject: [PATCH 280/512] Added release notes --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ee1a9973..1004ba57d 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -159,7 +159,8 @@ TODO Other Changes ============= -TODO -^^^^ +Support reading signed 8-bit TIFF images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +TIFF images with signed integer data, 8 bits per sample and a photometric +interpretaton of BlackIsZero can now be read. From 2467db492e7e50efaf39d445c92190c713e40f6f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 08:15:48 +1000 Subject: [PATCH 281/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7b13900a8..93517e1cd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,18 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added width argument to ImageDraw regular_polygon #7132 + [radarhere] + +- Support I mode for ImageFilter.BuiltinFilter #7108 + [radarhere] + +- Raise error from stderr of Linux ImageGrab.grabclipboard() command #7112 + [radarhere] + +- Added unpacker from I;16B to I;16 #7125 + [radarhere] + - Support float font sizes #7107 [radarhere] From 9f0c4164694ce99e14e7ec7fbfa8938acfb74823 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:30:17 +1000 Subject: [PATCH 282/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 93517e1cd..f8844daca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Support reading signed 8-bit TIFF images #7111 + [radarhere] + - Added width argument to ImageDraw regular_polygon #7132 [radarhere] From 3ae321832a52cf185ae2c10a5b17f7ea1c6f2dbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 4 May 2023 15:37:35 +1000 Subject: [PATCH 283/512] Added release notes for #7132 --- docs/releasenotes/10.0.0.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 1004ba57d..e2005b710 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -135,10 +135,11 @@ TODO API Changes =========== -TODO -^^^^ +Added line width parameter to ImageDraw regular_polygon +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +An optional line ``width`` parameter has been added to +``ImageDraw.Draw.regular_polygon``. API Additions ============= From 5377b0735f44ad78184a1b0092a327e22203a02a Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Thu, 4 May 2023 21:43:57 +0530 Subject: [PATCH 284/512] add _repr_jpg_ for ipython display Signed-off-by: Ishant Mrinal Haloi --- Tests/test_file_jpeg.py | 13 +++++++++++++ src/PIL/Image.py | 23 ++++++++++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 73a00386f..3676c8f07 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,6 +922,19 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False + def test_repr_jpg(self): + im = hopper() + + with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: + assert repr_jpg.format == "JPEG" + assert_image_equal(im, repr_jpg) + + def test_repr_jpg_error(self): + im = hopper("F") + + with pytest.raises(ValueError): + im._repr_jpg_() + @pytest.mark.skipif(not is_win32(), reason="Windows only") @skip_unless_feature("jpg") diff --git a/src/PIL/Image.py b/src/PIL/Image.py index bee9e23d0..557810f6c 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,19 +633,36 @@ class Image: ) ) - def _repr_png_(self): + def _repr_image(self, format): """iPython display hook support + :param format: Image format. :returns: png version of the image as bytes """ b = io.BytesIO() try: - self.save(b, "PNG") + self.save(b, format) except Exception as e: - msg = "Could not save to PNG for display" + msg = f"Could not save to {format} for display" raise ValueError(msg) from e return b.getvalue() + def _repr_png_(self): + """iPython display hook support for PNG format. + + :returns: png version of the image as bytes + """ + return self._repr_image("PNG") + + def _repr_jpg_(self): + """iPython display hook support for JPEG format. + + :returns: jpg version of the image as bytes + """ + return self._repr_image("JPEG") + + _repr_jpeg_ = _repr_jpg_ + @property def __array_interface__(self): # numpy array interface support From c5f90af56c7ca2b0e1ee3d3a95b5e7cd5291df5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 07:19:13 +1000 Subject: [PATCH 285/512] Updated xz to 5.4.3 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d18..05df77a68 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.2.tar.gz/download", - "filename": "xz-5.4.2.tar.gz", - "dir": "xz-5.4.2", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", + "filename": "xz-5.4.3.tar.gz", + "dir": "xz-5.4.3", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), From 93e507294bb5f54ea8ac43a596fd0d2677e2cdad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 08:19:43 +1000 Subject: [PATCH 286/512] Only assert image is similar --- Tests/test_file_jpeg.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 3676c8f07..0247527f5 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -922,18 +922,18 @@ class TestFileJpeg: im.load() ImageFile.LOAD_TRUNCATED_IMAGES = False - def test_repr_jpg(self): + def test_repr_jpeg(self): im = hopper() - with Image.open(BytesIO(im._repr_jpg_())) as repr_jpg: - assert repr_jpg.format == "JPEG" - assert_image_equal(im, repr_jpg) + with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: + assert repr_jpeg.format == "JPEG" + assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpg_error(self): + def test_repr_jpeg_error(self): im = hopper("F") with pytest.raises(ValueError): - im._repr_jpg_() + im._repr_jpeg_() @pytest.mark.skipif(not is_win32(), reason="Windows only") From 04191d15f6fee33c50536991e734454195c2da8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 17:54:42 +1000 Subject: [PATCH 287/512] Removed separate test for array tobytes() --- Tests/test_imagepath.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 7a517b6f6..5082f9a79 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -63,17 +63,6 @@ def test_path_constructors(coords): assert list(p) == [(0.0, 1.0)] -def test_path_constructor_text(): - # Arrange - arr = array.array("f", (0, 1)) - - # Act - p = ImagePath.Path(arr.tobytes()) - - # Assert - assert list(p) == [(0.0, 1.0)] - - @pytest.mark.parametrize( "coords", ( From 17fbafb10b6fbb7d364ff4e6474149c12bc03a42 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:12:10 +1000 Subject: [PATCH 288/512] Updated ImagePath tolist() default --- docs/reference/ImagePath.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/ImagePath.rst b/docs/reference/ImagePath.rst index 7c1a3ad70..500096ef7 100644 --- a/docs/reference/ImagePath.rst +++ b/docs/reference/ImagePath.rst @@ -48,7 +48,7 @@ vector data. Path objects can be passed to the methods on the Maps the path through a function. -.. py:method:: PIL.ImagePath.Path.tolist(flat=0) +.. py:method:: PIL.ImagePath.Path.tolist(flat=False) Converts the path to a Python list [(x, y), …]. From 38c40d81d2d0a97e208c7f3bcf468a81176f6288 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 5 May 2023 18:25:05 +1000 Subject: [PATCH 289/512] Use boolean instead of integer --- Tests/test_imagepath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 8f8a9f449..5c40d4756 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -28,7 +28,7 @@ def test_path(): (6.0, 7.0), (8.0, 9.0), ] - assert p.tolist(1) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + assert p.tolist(True) == [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] assert p.getbbox() == (0.0, 1.0, 8.0, 9.0) From 2d841e16c2d7b16f6fe0b156c79a13affd9ac630 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:31:58 +0530 Subject: [PATCH 290/512] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- 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 557810f6c..3c0094817 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -637,7 +637,7 @@ class Image: """iPython display hook support :param format: Image format. - :returns: png version of the image as bytes + :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: From ccdce1791dab1e754df17d21a771c8ac073b7c58 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 6 May 2023 10:35:28 +0530 Subject: [PATCH 291/512] rename format to image_format --- src/PIL/Image.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 3c0094817..33984e594 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -633,17 +633,17 @@ class Image: ) ) - def _repr_image(self, format): + def _repr_image(self, image_format): """iPython display hook support - :param format: Image format. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ b = io.BytesIO() try: - self.save(b, format) + self.save(b, image_format) except Exception as e: - msg = f"Could not save to {format} for display" + msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e return b.getvalue() From f67fcf131a53f7436a2f4a540ed251c927af2c05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 11:58:05 +1000 Subject: [PATCH 292/512] If the clipboard fails to open on Windows, wait and try again --- src/display.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/display.c b/src/display.c index e8e7b62c2..754a6ae78 100644 --- a/src/display.c +++ b/src/display.c @@ -437,8 +437,14 @@ PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) { LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL}; if (!OpenClipboard(NULL)) { - PyErr_SetString(PyExc_OSError, "failed to open clipboard"); - return NULL; + // Maybe the clipboard is temporarily in use by another process. + // Wait and try again + Sleep(500); + + if (!OpenClipboard(NULL)) { + PyErr_SetString(PyExc_OSError, "failed to open clipboard"); + return NULL; + } } // find best format as set by clipboard owner From 2f896ee4ac1f86cd05659c6a3053d38ed0b15aff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 8 May 2023 21:16:34 +1000 Subject: [PATCH 293/512] Clarify that line() and polygon() include xy pixels --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 29115120c..524f821fb 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -243,6 +243,7 @@ Methods .. py:method:: ImageDraw.line(xy, fill=None, width=0, joint=None) Draws a line between the coordinates in the ``xy`` list. + The coordinate pixels are included in the drawn line. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. @@ -287,7 +288,7 @@ Methods The polygon outline consists of straight lines between the given coordinates, plus a straight line between the last and the first - coordinate. + coordinate. The coordinate pixels are included in the drawn polygon. :param xy: Sequence of either 2-tuples like ``[(x, y), (x, y), ...]`` or numeric values like ``[x, y, x, y, ...]``. From bb18abc603a285b9c1c3705f03d7ee3955f935d7 Mon Sep 17 00:00:00 2001 From: nulano Date: Mon, 8 May 2023 22:30:11 +0100 Subject: [PATCH 294/512] prefer screenshots using XCB over gnome-screenshot --- docs/reference/ImageGrab.rst | 5 +++-- src/PIL/ImageGrab.py | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..6437307c0 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -15,8 +15,9 @@ or the clipboard to a PIL image memory. returned as an "RGBA" on macOS, or an "RGB" image otherwise. If the bounding box is omitted, the entire screen is copied. - On Linux, if ``xdisplay`` is ``None`` then ``gnome-screenshot`` will be used if it - is installed. To capture the default X11 display instead, pass ``xdisplay=""``. + On Linux, if ``xdisplay`` is ``None`` and the default X11 display does not return + a snapshot of the screen, ``gnome-screenshot`` will be used as fallback if it is + installed. To disable this behaviour, pass ``xdisplay=""`` instead. .. versionadded:: 1.1.3 (Windows), 3.0.0 (macOS), 7.1.0 (Linux) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 2592ba2df..db993836d 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -61,7 +61,17 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N left, top, right, bottom = bbox im = im.crop((left - x0, top - y0, right - x0, bottom - y0)) return im - elif shutil.which("gnome-screenshot"): + try: + if not Image.core.HAVE_XCB: + msg = "Pillow was built without XCB support" + raise OSError(msg) + size, data = Image.core.grabscreen_x11(xdisplay) + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im + except OSError: + if xdisplay is None and shutil.which("gnome-screenshot"): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) @@ -73,15 +83,8 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im.close() return im_cropped return im - # use xdisplay=None for default display on non-win32/macOS systems - if not Image.core.HAVE_XCB: - msg = "Pillow was built without XCB support" - raise OSError(msg) - size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im + else: + raise def grabclipboard(): From a0b691a219d274a561784781476cc952e79c1b8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 9 May 2023 12:12:16 +1000 Subject: [PATCH 295/512] Fixed combining single duration across duplicate PNG frames --- Tests/test_file_apng.py | 6 ++++++ src/PIL/PngImagePlugin.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f78c086eb..c62231cd4 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,6 +440,12 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 + # test removal of duplicated frames with a single duration + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 82a74b267..aaf242b1d 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1146,11 +1146,14 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) and prev_disposal == encoderinfo.get("disposal") and prev_blend == encoderinfo.get("blend") ): - if isinstance(duration, (list, tuple)): - previous["encoderinfo"]["duration"] += encoderinfo["duration"] + previous["encoderinfo"]["duration"] += encoderinfo.get( + "duration", duration + ) continue else: bbox = None + if "duration" not in encoderinfo: + encoderinfo["duration"] = duration im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo}) # animation control @@ -1175,7 +1178,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) im_frame = im_frame.crop(bbox) size = im_frame.size encoderinfo = frame_data["encoderinfo"] - frame_duration = int(round(encoderinfo.get("duration", duration))) + frame_duration = int(round(encoderinfo["duration"])) frame_disposal = encoderinfo.get("disposal", disposal) frame_blend = encoderinfo.get("blend", blend) # frame control From b9b685fc5699cbeb18f18096e53a1130d1cd6789 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 12:35:59 +1000 Subject: [PATCH 296/512] Updated harfbuzz to 7.3.0 --- winbuild/build_prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9b5fc5d18..1e5a54f64 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -337,9 +337,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.2.0.zip", - "filename": "harfbuzz-7.2.0.zip", - "dir": "harfbuzz-7.2.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", + "filename": "harfbuzz-7.3.0.zip", + "dir": "harfbuzz-7.3.0", "license": "COPYING", "build": [ *cmds_cmake( From c68c508e27935f31ffa357e7e3bc7763cbe461e5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 10 May 2023 13:25:35 +1000 Subject: [PATCH 297/512] Fixed joined corners for odd dimensions --- .../images/imagedraw_rounded_rectangle_x_odd.png | Bin 0 -> 565 bytes .../images/imagedraw_rounded_rectangle_y_odd.png | Bin 0 -> 527 bytes Tests/test_imagedraw.py | 2 ++ src/PIL/ImageDraw.py | 4 ++-- 4 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_x_odd.png create mode 100644 Tests/images/imagedraw_rounded_rectangle_y_odd.png diff --git a/Tests/images/imagedraw_rounded_rectangle_x_odd.png b/Tests/images/imagedraw_rounded_rectangle_x_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..f23f1945e1fec32362830cc6e872c08956fe7758 GIT binary patch literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^DImiE{w+sGn_n7?5SDmN3P}2njzRbCo#{c2^^YHh}*S}w5dCT|M zE9R}USk2jA&%3xWWowR(cyO##q+A$Fa4b`#&^6JO{qORB7e5qTwD-`)Up}=9!fJO| zY>HUZcg1$uF22YK;R<2iS#wsda}|3i+FGjhGgMXbv*XuKnWT5Ms_^&yc>Az@ zA4^o_6StUTA+6Q{U6F?LqSp{H{qj(<0ZTvZx)z3Ao!v58r+w;qX>O8ZUel0MKZ z`qzKc(vOPHX4$`@a&I0tNU%*>yN%8LYyLH*?5$5_)&3M;tIrb2-hRV-?bT~}OD0-R zJS)WH&7reUam`oxi0F04#b&FrOcRK3O5kiHi@}@tc3)ZP)6>g>Z}FD>zMYf)GjX6^Gd-AAG-p+a`)s@1J==Xy$i$t|mNo$vAZ{+h@*&h+msciMi&SuU3; zteYmFzL~S|*?fWJo?L}~D+Saaa20ObB(S`UyHL(X5C|?#{>56pvSWg^N}?GsIWTy- L`njxgN@xNA*hcbx literal 0 HcmV?d00001 diff --git a/Tests/images/imagedraw_rounded_rectangle_y_odd.png b/Tests/images/imagedraw_rounded_rectangle_y_odd.png new file mode 100644 index 0000000000000000000000000000000000000000..96441bc7289eb3e70f23df0f9569a72ca407046d GIT binary patch literal 527 zcmeAS@N?(olHy`uVBq!ia0vp^DIm66s@eeHX4ax2z2 zHumxGnrmG<+MA`k>ge%ZwwI%FHl2v*Kea_{s*cXIrB|NMkUq8kp?mumu8z`qnE?xw zpFgyk{PmcXW@+KFt&>x1_wGuPox02L@~uFla{c->cZ2v(ZrL{b%=OoYw>Rl5oz@)s zv#zi@NmVP<;DC1H)>?8~Y_h7tw1x<#B+d>5Bjd=enzOk-pU;aiv$pylHUFfL zfVYE=qxH2T+jU>IZ8eA#@?T@wc6uXs@S4--s_&fG#Vopy=IX&tG`Z(a= x1 - x0 + full_x = d >= x1 - x0 - 1 if full_x: # The two left and two right corners are joined d = x1 - x0 - full_y = d >= y1 - y0 + full_y = d >= y1 - y0 - 1 if full_y: # The two top and two bottom corners are joined d = y1 - y0 From 3ec03c6720e4a4a0d4bd5a893ccac6b1df14418a Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 10 May 2023 13:53:55 +1000 Subject: [PATCH 298/512] Only check for gnome-screenshot on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> --- src/PIL/ImageGrab.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index db993836d..361077110 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -71,7 +71,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N im = im.crop(bbox) return im except OSError: - if xdisplay is None and shutil.which("gnome-screenshot"): + if ( + xdisplay is None + and sys.platform not in ("darwin", "win32") + and shutil.which("gnome-screenshot") + ): fh, filepath = tempfile.mkstemp(".png") os.close(fh) subprocess.call(["gnome-screenshot", "-f", filepath]) From 8bbccba8250d4ec05a88fce3381f85c7dbb882e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 08:13:33 +1000 Subject: [PATCH 299/512] Updated redirected URL --- docs/deprecations.rst | 2 +- docs/releasenotes/10.0.0.rst | 2 +- docs/releasenotes/9.2.0.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 45b2f4200..62687d869 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -210,7 +210,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ~~~~~~~~~~~~~~ diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710..d71ca0fa6 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -117,7 +117,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been removed from ``ImageQt``. Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. Image.coerce_e ^^^^^^^^^^^^^^ diff --git a/docs/releasenotes/9.2.0.rst b/docs/releasenotes/9.2.0.rst index 8d8bfc9f8..b875edf8e 100644 --- a/docs/releasenotes/9.2.0.rst +++ b/docs/releasenotes/9.2.0.rst @@ -15,7 +15,7 @@ open-source users (and will reach EOL on 2023-12-08 for commercial licence holde Support for PyQt5 and PySide2 has been deprecated from ``ImageQt`` and will be removed in Pillow 10 (2023-07-01). Upgrade to `PyQt6 `_ or -`PySide6 `_ instead. +`PySide6 `_ instead. FreeTypeFont.getmask2 fill parameter ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 848fd7c2dbaf07bc8e0a24d8efe1400cc8140f99 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 16:49:08 +1000 Subject: [PATCH 300/512] Added linkcheck_allowed_redirects --- docs/conf.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 2ebcd6b2e..a2c825292 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -317,6 +317,17 @@ def setup(app): app.add_css_file("css/dark.css") +linkcheck_allowed_redirects = { + r"https://bestpractices.coreinfrastructure.org/projects/6331": r"https://bestpractices.coreinfrastructure.org/en/.*", # noqa: E501 + r"https://badges.gitter.im/python-pillow/Pillow.svg": r"https://badges.gitter.im/repo.svg", # noqa: E501 + r"https://gitter.im/python-pillow/Pillow?.*": r"https://app.gitter.im/#/room/#python-pillow_Pillow:gitter.im?.*", # noqa: E501 + r"https://pillow.readthedocs.io/?badge=latest": r"https://pillow.readthedocs.io/en/stable/?badge=latest", # noqa: E501 + r"https://pillow.readthedocs.io": r"https://pillow.readthedocs.io/en/stable/", + r"https://tidelift.com/badges/package/pypi/Pillow?.*": r"https://img.shields.io/badge/.*", # noqa: E501 + r"https://zenodo.org/badge/17549/python-pillow/Pillow.svg": r"https://zenodo.org/badge/doi/[\.0-9]+/zenodo.[0-9]+.svg", # noqa: E501 + r"https://zenodo.org/badge/latestdoi/17549/python-pillow/Pillow": r"https://zenodo.org/record/[0-9]+", # noqa: E501 +} + # sphinx.ext.extlinks # This config is a dictionary of external sites, # mapping unique short aliases to a base URL and a prefix. From 7e29efd518b0b5b2d3c5ed25446f12cf90f63338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 11 May 2023 20:08:10 +1000 Subject: [PATCH 301/512] Do not catch OSError raised when loading image --- src/PIL/ImageGrab.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 361077110..a51294cb5 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -66,10 +66,6 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N msg = "Pillow was built without XCB support" raise OSError(msg) size, data = Image.core.grabscreen_x11(xdisplay) - im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) - if bbox: - im = im.crop(bbox) - return im except OSError: if ( xdisplay is None @@ -89,6 +85,11 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N return im else: raise + else: + im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1) + if bbox: + im = im.crop(bbox) + return im def grabclipboard(): From 46708099b10a99b3d35d7eae624e83daa6d67bf9 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Fri, 12 May 2023 21:56:40 +0530 Subject: [PATCH 302/512] Apply suggestions from code review Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 33984e594..21305d52a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -654,15 +654,13 @@ class Image: """ return self._repr_image("PNG") - def _repr_jpg_(self): + def _repr_jpeg_(self): """iPython display hook support for JPEG format. :returns: jpg version of the image as bytes """ return self._repr_image("JPEG") - _repr_jpeg_ = _repr_jpg_ - @property def __array_interface__(self): # numpy array interface support From e063ed772c0be2b878e97d55b69d59c573cd40fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 11:02:53 +1000 Subject: [PATCH 303/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f8844daca..7dd99af99 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 + [radarhere] + - Support reading signed 8-bit TIFF images #7111 [radarhere] From 2db9c68571f3be1d29888b3497f4e2af518a2d36 Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sat, 13 May 2023 07:32:02 +0530 Subject: [PATCH 304/512] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič <3819630+nulano@users.noreply.github.com> Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/Image.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 21305d52a..3522ff6d0 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,8 +634,7 @@ class Image: ) def _repr_image(self, image_format): - """iPython display hook support - + """Helper function for iPython display hook :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -657,7 +656,7 @@ class Image: def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpg version of the image as bytes + :returns: jpeg version of the image as bytes """ return self._repr_image("JPEG") From 59b7a48570cd9c7a2fae1e0876c5072bdd780338 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 12:24:50 +1000 Subject: [PATCH 305/512] Updated docstrings --- 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 3522ff6d0..105c83a8b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -634,7 +634,8 @@ class Image: ) def _repr_image(self, image_format): - """Helper function for iPython display hook + """Helper function for iPython display hook. + :param image_format: Image format. :returns: image as bytes, saved into the given format. """ @@ -1122,7 +1123,6 @@ class Image: Available methods are :data:`Dither.NONE` or :data:`Dither.FLOYDSTEINBERG` (default). :returns: A new image - """ self.load() From 6df8716025686fab55b7565df489e01f3a23e5b4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 13 May 2023 21:38:01 +1000 Subject: [PATCH 306/512] Update grabclipboard() documentation after #6783 --- docs/reference/ImageGrab.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageGrab.rst b/docs/reference/ImageGrab.rst index 3086ba8c3..10c580a74 100644 --- a/docs/reference/ImageGrab.rst +++ b/docs/reference/ImageGrab.rst @@ -39,9 +39,11 @@ or the clipboard to a PIL image memory. .. py:function:: grabclipboard() - Take a snapshot of the clipboard image, if any. Only macOS and Windows are currently supported. + Take a snapshot of the clipboard image, if any. - .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS) + On Linux, ``wl-paste`` or ``xclip`` is required. + + .. versionadded:: 1.1.4 (Windows), 3.3.0 (macOS), 9.4.0 (Linux) :return: On Windows, an image, a list of filenames, or None if the clipboard does not contain image data or filenames. @@ -49,3 +51,5 @@ or the clipboard to a PIL image memory. On Mac, an image, or None if the clipboard does not contain image data. + + On Linux, an image. From f3283837630e25f88f1d8f73961c898d88ab1aee Mon Sep 17 00:00:00 2001 From: Ishant Mrinal Haloi Date: Sun, 14 May 2023 11:11:56 +0530 Subject: [PATCH 307/512] Apply suggestions from code review Co-authored-by: Hugo van Kemenade --- 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 105c83a8b..e0fb6a885 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -650,14 +650,14 @@ class Image: def _repr_png_(self): """iPython display hook support for PNG format. - :returns: png version of the image as bytes + :returns: PNG version of the image as bytes """ return self._repr_image("PNG") def _repr_jpeg_(self): """iPython display hook support for JPEG format. - :returns: jpeg version of the image as bytes + :returns: JPEG version of the image as bytes """ return self._repr_image("JPEG") From 9754c8d18dc8f013193d18810b9de4f2b68694ff Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 14 May 2023 22:42:39 +1000 Subject: [PATCH 308/512] Added release notes --- docs/releasenotes/10.0.0.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index e2005b710..ececfa20d 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -160,6 +160,18 @@ TODO Other Changes ============= +Support display_jpeg() in IPython +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In addition to ``display()`` and ``display_png``, ``display_jpeg()`` can now +also be used to display images in IPython:: + + from PIL import Image + from IPython.display import display_jpeg + + im = Image.new("RGB", (100, 100), (255, 0, 0)) + display_jpeg(im) + Support reading signed 8-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From ce22ad96b71dd3a64ed65d00f36640b81beafd31 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 15 May 2023 09:47:09 +1000 Subject: [PATCH 309/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7dd99af99..b3a6c45a4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Prefer screenshots using XCB over gnome-screenshot #7143 + [nulano, radarhere] + - Fixed joined corners for ImageDraw rounded_rectangle() odd dimensions #7151 [radarhere] From 82e57b8a90bf0c062f6fb87b4bc3ed465e2b2c88 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Fri, 28 Apr 2023 12:53:17 +0300 Subject: [PATCH 310/512] Build only PDF in addition to default html --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index ec3300dd1..bda03d944 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,6 @@ version: 2 -formats: all +formats: [pdf] build: os: ubuntu-22.04 From ac2d283065e38672a844730e0b11dd9835d588b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 16 May 2023 07:08:02 +1000 Subject: [PATCH 311/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b3a6c45a4..c67b9b432 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Use "/sbin/ldconfig" if ldconfig is not found #7068 + [radarhere] + - Prefer screenshots using XCB over gnome-screenshot #7143 [nulano, radarhere] From 53e73fd0941a8148e293245dfe9edf8412dacbaa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:21:59 +1000 Subject: [PATCH 312/512] Updated fribidi to 1.0.13 --- winbuild/build_prepare.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 1e5a54f64..19552f3c7 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -352,12 +352,12 @@ deps = { "libs": [r"*.lib"], }, "fribidi": { - "url": "https://github.com/fribidi/fribidi/archive/v1.0.12.zip", - "filename": "fribidi-1.0.12.zip", - "dir": "fribidi-1.0.12", + "url": "https://github.com/fribidi/fribidi/archive/v1.0.13.zip", + "filename": "fribidi-1.0.13.zip", + "dir": "fribidi-1.0.13", "license": "COPYING", "build": [ - cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.12-COPYING"), + cmd_copy(r"COPYING", r"{bin_dir}\fribidi-1.0.13-COPYING"), cmd_copy(r"{winbuild_dir}\fribidi.cmake", r"CMakeLists.txt"), *cmds_cmake("fribidi"), ], From 0e21e47768315f6af1afcbd88ee24a4ab68b5d36 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 08:25:25 +1000 Subject: [PATCH 313/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c67b9b432..c79274d64 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added `_repr_jpeg_` for IPython display_jpeg #7135 + [n3011, radarhere, nulano] + - Use "/sbin/ldconfig" if ldconfig is not found #7068 [radarhere] From 599979caae111c22bd15f0103320bf74eb53d963 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 17 May 2023 16:53:42 +1000 Subject: [PATCH 314/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index c79274d64..626b8b231 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Added `_repr_jpeg_` for IPython display_jpeg #7135 +- Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] - Use "/sbin/ldconfig" if ldconfig is not found #7068 From b39c807dde8909b1ce4afd85f37563165224e073 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 18 May 2023 22:14:40 +1000 Subject: [PATCH 315/512] Removed rectangle example from co-ordinate system documentation --- docs/handbook/concepts.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/handbook/concepts.rst b/docs/handbook/concepts.rst index e40ed4687..e0975a121 100644 --- a/docs/handbook/concepts.rst +++ b/docs/handbook/concepts.rst @@ -95,9 +95,8 @@ in the upper left corner. Note that the coordinates refer to the implied pixel corners; the centre of a pixel addressed as (0, 0) actually lies at (0.5, 0.5). Coordinates are usually passed to the library as 2-tuples (x, y). Rectangles -are represented as 4-tuples, with the upper left corner given first. For -example, a rectangle covering all of an 800x600 pixel image is written as (0, -0, 800, 600). +are represented as 4-tuples, (x1, y1, x2, y2), with the upper left corner given +first. Palette ------- From 509671c53e31d2fc9af6f89d1ac158cce8160d62 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 17:59:33 -0500 Subject: [PATCH 316/512] fix INT64 def and add warning if not set --- src/libImaging/ImPlatform.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..f183c3aa4 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -61,7 +61,9 @@ #if SIZEOF_LONG == 8 #define INT64 long #elif SIZEOF_LONG_LONG == 8 -#define INT64 long +#define INT64 long long +#else +#warning Cannot find required 64-bit integer type #endif #define INT8 signed char From 6de5e999bd7ee571877c975c9cb2a3038ee90f27 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:00:45 -0500 Subject: [PATCH 317/512] add UINT64 def if INT64 is defined --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index f183c3aa4..522776b58 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -71,6 +71,9 @@ #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 +#ifdef INT64 +#define UINT64 unsigned INT64 +#endif #endif From e9cfe4b6a2139923e3cab44bdd55cbf45ac16333 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:02:41 -0500 Subject: [PATCH 318/512] label preprocessor if..else..endif for clarity --- src/libImaging/ImPlatform.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 522776b58..6db251bf1 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -25,7 +25,7 @@ #endif #endif -#if defined(_WIN32) || defined(__CYGWIN__) +#if defined(_WIN32) || defined(__CYGWIN__) /* WIN */ #define WIN32_LEAN_AND_MEAN #include @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else +#else /* WIN */ /* For System that are not Windows, we'll need to define these. */ #if SIZEOF_SHORT == 2 @@ -75,7 +75,7 @@ #define UINT64 unsigned INT64 #endif -#endif +#endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From c2527348ecf4487be76fa55eefa440be1a96e9f5 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:04:10 -0500 Subject: [PATCH 319/512] add comment explaining why #define and not typedef --- src/libImaging/ImPlatform.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 6db251bf1..e2d2d597b 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -39,6 +39,9 @@ #else /* WIN */ /* For System that are not Windows, we'll need to define these. */ +/* We have to define them instead of using typedef because the JPEG lib also + defines their own types with the same names, so we need to be able to undef + ours before including the JPEG code. */ #if SIZEOF_SHORT == 2 #define INT16 short From fbec8f19dd1ec77e3cf741c11038f0229967830c Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:11:02 -0500 Subject: [PATCH 320/512] add check for C99+ to use their defs if possible --- src/libImaging/ImPlatform.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index e2d2d597b..9f736ed75 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -43,6 +43,23 @@ defines their own types with the same names, so we need to be able to undef ours before including the JPEG code. */ +#if __STDC_VERSION__ >= 199901L /* C99+ */ + +#include + +#define INT8 int8_t +#define UINT8 uint8_t +#define INT16 int16_t +#define UINT16 uint16_t +#define INT32 int32_t +#define UINT32 uint32_t +#ifdef INT64_MAX +#define INT64 int64_t +#define UINT64 uint64_t +#endif + +#else /* C99+ */ + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -78,6 +95,8 @@ #define UINT64 unsigned INT64 #endif +#endif /* C99+ */ + #endif /* WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ From 9da0b58eea9d59d725106a76702b592e302c9665 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:13:50 -0500 Subject: [PATCH 321/512] move INT8 def to top --- src/libImaging/ImPlatform.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 9f736ed75..303ebf987 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -60,6 +60,8 @@ #else /* C99+ */ +#define INT8 signed char + #if SIZEOF_SHORT == 2 #define INT16 short #elif SIZEOF_INT == 2 @@ -86,9 +88,7 @@ #warning Cannot find required 64-bit integer type #endif -#define INT8 signed char #define UINT8 unsigned char - #define UINT16 unsigned INT16 #define UINT32 unsigned INT32 #ifdef INT64 From 724f2664601708dd1355c8a03b3815d56d788609 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 6 Oct 2022 18:14:55 -0500 Subject: [PATCH 322/512] change INT16 def failure to an error --- src/libImaging/ImPlatform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 303ebf987..912b17855 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -67,7 +67,7 @@ #elif SIZEOF_INT == 2 #define INT16 int #else -#define INT16 short /* most things works just fine anyway... */ +#error Cannot find required 16-bit integer type #endif #if SIZEOF_SHORT == 4 From f6b516bb068f28134c781b42f4188931f9e11b6b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 19 May 2023 08:01:02 -0500 Subject: [PATCH 323/512] Adjust C preprocessor block labels Co-authored-by: Hugo van Kemenade --- src/libImaging/ImPlatform.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 912b17855..10d6905b9 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -37,7 +37,7 @@ #undef WIN32 #endif -#else /* WIN */ +#else /* not WIN */ /* For System that are not Windows, we'll need to define these. */ /* We have to define them instead of using typedef because the JPEG lib also defines their own types with the same names, so we need to be able to undef @@ -58,7 +58,7 @@ #define UINT64 uint64_t #endif -#else /* C99+ */ +#else /* < C99 */ #define INT8 signed char @@ -95,9 +95,9 @@ #define UINT64 unsigned INT64 #endif -#endif /* C99+ */ +#endif /* < C99 */ -#endif /* WIN */ +#endif /* not WIN */ /* assume IEEE; tweak if necessary (patches are welcome) */ #define FLOAT16 UINT16 From 4f734d295f7ce2e31c650e1971b45dae7f19c8d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:38:36 +1000 Subject: [PATCH 324/512] Use --config-settings instead of deprecated --global-option --- .github/workflows/test-mingw.yml | 2 +- MANIFEST.in | 1 + Makefile | 4 ++-- _custom_build/backend.py | 31 +++++++++++++++++++++++++++++++ docs/installation.rst | 24 ++++++++++++------------ pyproject.toml | 4 ++++ 6 files changed, 51 insertions(+), 15 deletions(-) create mode 100755 _custom_build/backend.py create mode 100644 pyproject.toml diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a109ec0d8..5a737a1ee 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -80,7 +80,7 @@ jobs: pushd depends && ./install_extra_test_images.sh && popd - name: Build Pillow - run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install --global-option="build_ext" . + run: SETUPTOOLS_USE_DISTUTILS="stdlib" CFLAGS="-coverage" python3 -m pip install . - name: Test Pillow run: | diff --git a/MANIFEST.in b/MANIFEST.in index f51551303..606e7e074 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ graft src graft depends graft winbuild graft docs +graft _custom_build # build/src control detritus exclude .appveyor.yml diff --git a/Makefile b/Makefile index e41f36411..776649b28 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ install: .PHONY: install-coverage install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install --global-option="build_ext" . + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . python3 selftest.py .PHONY: debug @@ -74,7 +74,7 @@ debug: # for our stuff, kills optimization, and redirects to dev null so we # see any build failures. make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install --global-option="build_ext" . > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null .PHONY: release-test release-test: diff --git a/_custom_build/backend.py b/_custom_build/backend.py new file mode 100755 index 000000000..31a954824 --- /dev/null +++ b/_custom_build/backend.py @@ -0,0 +1,31 @@ +import sys + +from setuptools.build_meta import * # noqa: F401, F403 +from setuptools.build_meta import _BuildMetaBackend + + +class _CustomBuildMetaBackend(_BuildMetaBackend): + def run_setup(self, setup_script="setup.py"): + if self.config_settings: + flags = [] + for key in ("enable", "disable", "vendor"): + settings = self.config_settings.get(key) + if settings: + if not isinstance(settings, list): + settings = [settings] + for value in settings: + flags.append("--" + key + "-" + value) + if self.config_settings.get("debug") == "true": + flags.append("--debug") + if flags: + sys.argv = sys.argv[:1] + ["build_ext"] + flags + sys.argv[1:] + return super().run_setup(setup_script) + + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + self.config_settings = config_settings + return super().build_wheel(wheel_directory, config_settings, metadata_directory) + + +build_wheel = _CustomBuildMetaBackend().build_wheel diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67ee..514d20e74 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,40 +380,40 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Build flags: ``--disable-zlib``, ``--disable-jpeg``, - ``--disable-tiff``, ``--disable-freetype``, ``--disable-raqm``, - ``--disable-lcms``, ``--disable-webp``, ``--disable-webpmux``, - ``--disable-jpeg2000``, ``--disable-imagequant``, ``--disable-xcb``. +* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, + ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, + ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, + ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Build flags: ``--enable-zlib``, ``--enable-jpeg``, - ``--enable-tiff``, ``--enable-freetype``, ``--enable-raqm``, - ``--enable-lcms``, ``--enable-webp``, ``--enable-webpmux``, - ``--enable-jpeg2000``, ``--enable-imagequant``, ``--enable-xcb``. +* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, + ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, + ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, + ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Build flags: ``--vendor-raqm``, ``--vendor-fribidi``. +* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``--disable-platform-guessing``. Skips all of the +* Build flag: ``-C disable=platform-guessing``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). -* Build flag: ``--debug``. Adds a debugging flag to the include and +* Build flag: ``-C debug=true``. Adds a debugging flag to the include and library search process to dump all paths searched for and found to stdout. Sample usage:: - python3 -m pip install --upgrade Pillow --global-option="build_ext" --global-option="--enable-[feature]" + python3 -m pip install --upgrade Pillow -C enable=[feature] Platform Support ---------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..cf31b6407 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = ["setuptools >= 40.8.0", "wheel"] +build-backend = "backend" +backend-path = ["_custom_build"] From 18da2d0b2d01ab7bf9371451416379c0edca84f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 15:19:58 +1000 Subject: [PATCH 325/512] Removed inplace target --- Makefile | 5 ----- tox.ini | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 776649b28..57d756b47 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,6 @@ help: @echo " docserve run an HTTP server on the docs directory" @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" @echo " lint run the lint checks" @@ -54,10 +53,6 @@ help: @echo " release-test run code and package tests before release" @echo " test run tests on installed Pillow" -.PHONY: inplace -inplace: clean - python3 -m pip install -e --global-option="build_ext" --global-option="--inplace" . - .PHONY: install install: python3 -m pip -v install . diff --git a/tox.ini b/tox.ini index 458a00107..a79089f51 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ extras = tests commands = make clean - {envpython} -m pip install --global-option="build_ext" --global-option="--inplace" . + {envpython} -m pip install . {envpython} selftest.py {envpython} -m pytest -W always {posargs} allowlist_externals = From 546f6cbc27e178bfc742d8216a1af508b3e26e02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 May 2023 17:11:43 +1000 Subject: [PATCH 326/512] Replaced absolute PIL import with relative import --- src/PIL/IcnsImagePlugin.py | 4 ++-- src/PIL/ImageCms.py | 6 +++--- src/PIL/ImageShow.py | 2 +- src/PIL/SpiderImagePlugin.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index c2f050edd..27cb89f73 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -22,11 +22,11 @@ import os import struct import sys -from PIL import Image, ImageFile, PngImagePlugin, features +from . import Image, ImageFile, PngImagePlugin, features enable_jpeg2k = features.check_codec("jpg_2000") if enable_jpeg2k: - from PIL import Jpeg2KImagePlugin + from . import Jpeg2KImagePlugin MAGIC = b"icns" HEADERSIZE = 8 diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index 38cbab19c..3a337f9f2 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -18,10 +18,10 @@ import sys from enum import IntEnum -from PIL import Image +from . import Image try: - from PIL import _imagingcms + from . import _imagingcms except ImportError as ex: # Allow error import for doc purposes, but error out when accessing # anything in core. @@ -271,7 +271,7 @@ def get_display_profile(handle=None): if sys.platform != "win32": return None - from PIL import ImageWin + from . import ImageWin if isinstance(handle, ImageWin.HDC): profile = core.get_display_profile_win32(handle, 1) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 3f68a2696..8b1c3f8bb 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -17,7 +17,7 @@ import subprocess import sys from shlex import quote -from PIL import Image +from . import Image _viewers = [] diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index eac27e679..5614957c1 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -36,7 +36,7 @@ import os import struct import sys -from PIL import Image, ImageFile +from . import Image, ImageFile def isInt(f): @@ -191,7 +191,7 @@ class SpiderImageFile(ImageFile.ImageFile): # returns a ImageTk.PhotoImage object, after rescaling to 0..255 def tkPhotoImage(self): - from PIL import ImageTk + from . import ImageTk return ImageTk.PhotoImage(self.convert2byte(), palette=256) From 053cb3de52dcc5008e5bac96942697a1b7e9e00d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 May 2023 14:38:05 +1000 Subject: [PATCH 327/512] Fixed finding dependencies on Cygwin --- .github/workflows/test-cygwin.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 9a1e46705..e7ab6466e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -104,7 +104,7 @@ jobs: - name: Build shell: bash.exe -eo pipefail -o igncr "{0}" run: | - SETUPTOOLS_USE_DISTUTILS=stdlib .ci/build.sh + .ci/build.sh - name: Test run: | diff --git a/setup.py b/setup.py index 0b6b02077..7c1ad6dc5 100755 --- a/setup.py +++ b/setup.py @@ -515,6 +515,7 @@ class pil_build_ext(build_ext): elif sys.platform == "cygwin": # pythonX.Y.dll.a is in the /usr/lib/pythonX.Y/config directory + self.compiler.shared_lib_extension = ".dll.a" _add_directory( library_dirs, os.path.join( From dc6d0641b3c5e93b26b214a251754e549b84260a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 22 May 2023 19:39:25 +1000 Subject: [PATCH 328/512] Updated redirected URLs --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index f3afccc1c..b794632fa 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,9 @@ -# Documentation: https://docs.codecov.io/docs/codecov-yaml +# Documentation: https://docs.codecov.com/docs/codecov-yaml codecov: # Avoid "Missing base report" due to committing CHANGES.rst with "[CI skip]" # https://github.com/codecov/support/issues/363 - # https://docs.codecov.io/docs/comparing-commits + # https://docs.codecov.com/docs/comparing-commits allow_coverage_offsets: true comment: false From fffcb558f64f2350789b67ec5eb55681408a93d5 Mon Sep 17 00:00:00 2001 From: rrcgat Date: Tue, 23 May 2023 18:44:25 +0800 Subject: [PATCH 329/512] Use image/png mime type for ImageGrab (wl-paste) if possible, otherwise the first mime type taken --- src/PIL/ImageGrab.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 3771e6a79..b7f416321 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -147,15 +147,12 @@ def grabclipboard(): clipboard_mimetypes = output.splitlines() def find_mimetype(): - for mime in Image.MIME.values(): - if mime in clipboard_mimetypes: - return mime + if "image/png" in clipboard_mimetypes: + return "image/png" + if clipboard_mimetypes: + return clipboard_mimetypes[0] - Image.preinit() mimetype = find_mimetype() - if not mimetype: - Image.init() - mimetype = find_mimetype() if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From bce0f0d5a64c008b9d9ffbea33e98a79ffdae8c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:25:11 +1000 Subject: [PATCH 330/512] Moved function code inline --- src/PIL/ImageGrab.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index b7f416321..7f6d50af4 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -142,17 +142,16 @@ def grabclipboard(): return None else: if shutil.which("wl-paste"): - args = ["wl-paste"] output = subprocess.check_output(["wl-paste", "-l"]).decode() - clipboard_mimetypes = output.splitlines() + mimetypes = output.splitlines() + if "image/png" in mimetypes: + mimetype = "image/png" + elif mimetypes: + mimetype = mimetypes[0] + else: + mimetype = None - def find_mimetype(): - if "image/png" in clipboard_mimetypes: - return "image/png" - if clipboard_mimetypes: - return clipboard_mimetypes[0] - - mimetype = find_mimetype() + args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) elif shutil.which("xclip"): From 26d5f4fcb1fa23c920f42c56e187de092da544a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 21:27:55 +1000 Subject: [PATCH 331/512] Use tuple instead of list --- Tests/test_imagegrab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index 065c9c1b5..f8059eca4 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -102,7 +102,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes) @pytest.mark.skipif( ( sys.platform != "linux" - or not all(shutil.which(cmd) for cmd in ["wl-paste", "wl-copy"]) + or not all(shutil.which(cmd) for cmd in ("wl-paste", "wl-copy")) ), reason="Linux with wl-clipboard only", ) @@ -111,5 +111,5 @@ $ms = new-object System.IO.MemoryStream(, $bytes) image_path = "Tests/images/hopper." + ext with open(image_path, "rb") as fp: subprocess.call(["wl-copy"], stdin=fp) - im = ImageGrab.grabclipboard() - assert_image_equal_tofile(im, image_path) + im = ImageGrab.grabclipboard() + assert_image_equal_tofile(im, image_path) From b8719033ca91ef57f58128d32df675457431bbce Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 23 May 2023 22:53:16 +1000 Subject: [PATCH 332/512] Removed unused INT64 definition --- src/libImaging/ImPlatform.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index af9996ca9..94781f9ec 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -58,12 +58,6 @@ #error Cannot find required 32-bit integer type #endif -#if SIZEOF_LONG == 8 -#define INT64 long -#elif SIZEOF_LONG_LONG == 8 -#define INT64 long -#endif - #define INT8 signed char #define UINT8 unsigned char From 922e239cca2a45d239dd02f0a4b85b72a7918917 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 08:55:14 +1000 Subject: [PATCH 333/512] Fixed saving multiple 1 mode images to GIF --- Tests/test_file_gif.py | 13 +++++++++++++ src/PIL/GifImagePlugin.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 8522f486a..0e50ee1ab 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -252,6 +252,19 @@ def test_roundtrip_save_all(tmp_path): assert reread.n_frames == 5 +def test_roundtrip_save_all_1(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("1", (1, 1)) + im2 = Image.new("1", (1, 1), 1) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reloaded: + assert reloaded.getpixel((0, 0)) == 0 + + reloaded.seek(1) + assert reloaded.getpixel((0, 0)) == 255 + + @pytest.mark.parametrize( "path, mode", ( diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index eadee1560..2f92e9467 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -879,7 +879,7 @@ def _get_palette_bytes(im): :param im: Image object :returns: Bytes, len<=768 suitable for inclusion in gif header """ - return im.palette.palette + return im.palette.palette if im.palette else b"" def _get_background(im, info_background): From 117618b01f959f016833158b7b128e896c6d38b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 May 2023 22:47:43 +1000 Subject: [PATCH 334/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 626b8b231..190751ad2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Improved wl-paste mimetype handling in ImageGrab #7094 + [rrcgat, radarhere] + - Added _repr_jpeg_() for IPython display_jpeg #7135 [n3011, radarhere, nulano] From e6d7f1f3477b915d4f2fb7d71d609af74e47a444 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:52:13 +1000 Subject: [PATCH 335/512] Install setuptools on Windows --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a00880111..076b80839 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 7a5ddc1712240b21d89581602acbb851c3897e4a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 27 May 2023 10:28:38 +1000 Subject: [PATCH 336/512] Do not test PyQt6 on Python 3.12 --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index d5cbd8248..6e87d386d 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then 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 + if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi From 26e0c81ffb5ffa23711e7a4c244498307e13683c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 22:40:37 +0300 Subject: [PATCH 337/512] Revert "Install setuptools on Windows" This reverts commit e6d7f1f3477b915d4f2fb7d71d609af74e47a444. --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076b80839..a00880111 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -65,8 +65,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install setuptools wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 454dfcc1b4a60e4a56b50eaca0944049dacfc893 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 11:52:03 +0300 Subject: [PATCH 338/512] Add minimal pyproject.toml --- .pre-commit-config.yaml | 11 +++++++++++ pyproject.toml | 5 +++++ 2 files changed, 16 insertions(+) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317f..f4b695883 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -49,6 +49,7 @@ repos: hooks: - id: check-merge-conflict - id: check-json + - id: check-toml - id: check-yaml - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -56,6 +57,16 @@ repos: hooks: - id: sphinx-lint + - repo: https://github.com/tox-dev/pyproject-fmt + rev: 0.11.2 + hooks: + - id: pyproject-fmt + + - repo: https://github.com/abravalheri/validate-pyproject + rev: v0.13 + hooks: + - id: validate-pyproject + - repo: https://github.com/tox-dev/tox-ini-fmt rev: 1.3.0 hooks: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..8ed72aad7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", +] From 18960e8416dc95774b7e2616baf780c62304299d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 12:58:13 +0300 Subject: [PATCH 339/512] Use latest setuptools 67.8 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ed72aad7..59eb08fa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] build-backend = "setuptools.build_meta" requires = [ - "setuptools>=61.2", + "setuptools>=67.8", ] From 9ea7721a71b8b17086d5113861b509ec3bc1454d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 15:16:55 +0300 Subject: [PATCH 340/512] Replace direct invocation of setup.py --- winbuild/build_prepare.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 19552f3c7..12d2efbbc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,7 +569,10 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 + r'"{python_dir}\{python_exe}" -m pip install . ' + r'--global-option="--vendor-raqm" ' + r'--global-option="--vendor-fribidi" ' + r'--global-option="%*"', ] write_script("build_pillow.cmd", lines) From 3a0881dffe589fe745ccc6f68f4b2b74cf72f15e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 16:55:28 +0300 Subject: [PATCH 341/512] Disable extra quotes --- setup.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 0b6b02077..522c20991 100755 --- a/setup.py +++ b/setup.py @@ -847,14 +847,7 @@ class pil_build_ext(build_ext): if struct.unpack("h", b"\0\1")[0] == 1: defs.append(("WORDS_BIGENDIAN", None)) - if ( - sys.platform == "win32" - and sys.version_info < (3, 9) - and not (PLATFORM_PYPY or PLATFORM_MINGW) - ): - defs.append(("PILLOW_VERSION", f'"\\"{PILLOW_VERSION}\\""')) - else: - defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) + defs.append(("PILLOW_VERSION", f'"{PILLOW_VERSION}"')) self._update_extension("PIL._imaging", libs, defs) From 58b8d6c4efef6874ff8be7a601c1dcec01239163 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sat, 27 May 2023 17:13:19 +0300 Subject: [PATCH 342/512] Upgrade pip --- winbuild/build_prepare.py | 1 + 1 file changed, 1 insertion(+) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 12d2efbbc..d902cc1fb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,6 +569,7 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + r'"{python_dir}\{python_exe}" -m pip install --upgrade pip', r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' From 07eccd9798387a79db84557102d34de2f2f4c28d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 26 May 2023 19:14:56 +1000 Subject: [PATCH 343/512] Fixed calling putpalette() on L and LA images before load() --- Tests/test_image_putpalette.py | 8 ++++++++ src/libImaging/Unpack.c | 2 ++ 2 files changed, 10 insertions(+) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index 3b29769a7..665e08a7e 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -32,6 +32,14 @@ def test_putpalette(): with pytest.raises(ValueError): palette("YCbCr") + with Image.open("Tests/images/hopper_gray.jpg") as im: + assert im.mode == "L" + im.putpalette(list(range(256)) * 3) + + with Image.open("Tests/images/la.tga") as im: + assert im.mode == "LA" + im.putpalette(list(range(256)) * 3) + def test_imagepalette(): im = hopper("P") diff --git a/src/libImaging/Unpack.c b/src/libImaging/Unpack.c index a0fa22c7d..206403ba6 100644 --- a/src/libImaging/Unpack.c +++ b/src/libImaging/Unpack.c @@ -1552,10 +1552,12 @@ static struct { {"P", "P;4L", 4, unpackP4L}, {"P", "P", 8, copy1}, {"P", "P;R", 8, unpackLR}, + {"P", "L", 8, copy1}, /* palette w. alpha */ {"PA", "PA", 16, unpackLA}, {"PA", "PA;L", 16, unpackLAL}, + {"PA", "LA", 16, unpackLA}, /* true colour */ {"RGB", "RGB", 24, ImagingUnpackRGB}, From 7f0c49a6c76b7f6fb2086823b01e85ed8e30c450 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 28 May 2023 21:14:39 +0300 Subject: [PATCH 344/512] Move pip upgrade to .appveyor.yml --- .appveyor.yml | 1 + winbuild/build_prepare.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.appveyor.yml b/.appveyor.yml index 36f5bd0ad..cb364af55 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -20,6 +20,7 @@ environment: install: - '%PYTHON%\%EXECUTABLE% --version' +- '%PYTHON%\%EXECUTABLE% -m pip install --upgrade pip' - 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:\ diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index d902cc1fb..12d2efbbc 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -569,7 +569,6 @@ def build_pillow(): *prefs["header"], cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" -m pip install --upgrade pip', r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' From d5e03cca885465f28a01feca00583fa1073a36c5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 28 May 2023 21:20:42 +0300 Subject: [PATCH 345/512] Wrap arguments before passing Co-authored-by: nulano --- .appveyor.yml | 2 +- .github/workflows/test-windows.yml | 6 +++--- winbuild/build_prepare.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index cb364af55..575b6caa6 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -72,7 +72,7 @@ before_deploy: - cd c:\pillow - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - cd c:\pillow\winbuild\ - - c:\pillow\winbuild\build\build_pillow.cmd bdist_wheel + - c:\pillow\winbuild\build\build_pillow.cmd --global-option="bdist_wheel" - cd c:\pillow - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index a00880111..fbfec8c13 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -166,8 +166,8 @@ jobs: - name: Build Pillow run: | $FLAGS="" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS="--disable-imagequant" } - & winbuild\build\build_pillow.cmd $FLAGS install + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS='--global-option="--disable-imagequant"' } + & winbuild\build\build_pillow.cmd $FLAGS --global-option="install" & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -231,7 +231,7 @@ jobs: ) ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - winbuild\\build\\build_pillow.cmd --disable-imagequant bdist_wheel + winbuild\\build\\build_pillow.cmd --global-option="--disable-imagequant" --global-option="bdist_wheel" shell: cmd - name: Upload wheel diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 12d2efbbc..21b6c10a5 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -572,7 +572,7 @@ def build_pillow(): r'"{python_dir}\{python_exe}" -m pip install . ' r'--global-option="--vendor-raqm" ' r'--global-option="--vendor-fribidi" ' - r'--global-option="%*"', + r"%*", ] write_script("build_pillow.cmd", lines) From c45019fe0ccbf54c925aba914329371a7f188a48 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 29 May 2023 12:28:03 +1000 Subject: [PATCH 346/512] Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 --- src/_imagingft.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index 78e3f7f10..80f862bb7 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -132,6 +132,27 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { return NULL; } +#if PY_MAJOR_VERSION > 3 || PY_MINOR_VERSION > 11 + PyConfig config; + PyConfig_InitPythonConfig(&config); + if (!PyArg_ParseTupleAndKeywords( + args, + kw, + "etf|nsy#n", + kwlist, + config.filesystem_encoding, + &filename, + &size, + &index, + &encoding, + &font_bytes, + &font_bytes_size, + &layout_engine)) { + PyConfig_Clear(&config); + return NULL; + } + PyConfig_Clear(&config); +#else if (!PyArg_ParseTupleAndKeywords( args, kw, @@ -147,6 +168,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { &layout_engine)) { return NULL; } +#endif self = PyObject_New(FontObject, &Font_Type); if (!self) { From e01a0195dd9f54b8174f322d47d4f618f0cf6c50 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jun 2023 22:53:07 +1000 Subject: [PATCH 347/512] Removed duplicate config --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 449530717..d74549fe2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,10 +13,6 @@ indent_style = space trim_trailing_whitespace = true -[*.rst] -# Four-space indentation -indent_size = 4 - [*.yml] # Two-space indentation indent_size = 2 From ea3e4242d8fd8bb5cfc9e528f863ce16e20b529f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 08:07:05 +1000 Subject: [PATCH 348/512] Removed files and types override --- .pre-commit-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4882a317f..0ddc6beb4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,9 +4,6 @@ repos: hooks: - id: black args: [--target-version=py38] - # Only .py files, until https://github.com/psf/black/issues/402 resolved - files: \.py$ - types: [] - repo: https://github.com/PyCQA/isort rev: 5.12.0 From 3693b84ba0b44f71119cec73c8517ec32d1774b5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Jun 2023 09:21:47 +1000 Subject: [PATCH 349/512] Lint fixes --- docs/Guardfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Guardfile b/docs/Guardfile index b689b079a..6cbf07b06 100755 --- a/docs/Guardfile +++ b/docs/Guardfile @@ -2,7 +2,7 @@ from livereload.compiler import shell from livereload.task import Task -Task.add('*.rst', shell('make html')) -Task.add('*/*.rst', shell('make html')) -Task.add('Makefile', shell('make html')) -Task.add('conf.py', shell('make html')) +Task.add("*.rst", shell("make html")) +Task.add("*/*.rst", shell("make html")) +Task.add("Makefile", shell("make html")) +Task.add("conf.py", shell("make html")) From 2d0b13b812eea5238a8df6dc03b3fa4a6c55559e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 4 Jun 2023 22:37:17 +1000 Subject: [PATCH 350/512] Swapped config key and value --- _custom_build/backend.py | 31 +++++++++++++++++++++++++++---- docs/installation.rst | 22 +++++++++++----------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 31a954824..86fe60817 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -7,14 +7,37 @@ from setuptools.build_meta import _BuildMetaBackend class _CustomBuildMetaBackend(_BuildMetaBackend): def run_setup(self, setup_script="setup.py"): if self.config_settings: - flags = [] - for key in ("enable", "disable", "vendor"): + + def config_has(key, value): settings = self.config_settings.get(key) if settings: if not isinstance(settings, list): settings = [settings] - for value in settings: - flags.append("--" + key + "-" + value) + return value in settings + + flags = [] + for dependency in ( + "zlib", + "jpeg", + "tiff", + "freetype", + "raqm", + "lcms", + "webp", + "webpmux", + "jpeg2000", + "imagequant", + "xcb", + ): + if config_has(dependency, "enable"): + flags.append("--enable-" + dependency) + elif config_has(dependency, "disable"): + flags.append("--disable-" + dependency) + for dependency in ("raqm", "fribidi"): + if config_has(dependency, "vendor"): + flags.append("--vendor-" + dependency) + if self.config_settings.get("platform-guessing") == "disable": + flags.append("--disable-platform-guessing") if self.config_settings.get("debug") == "true": flags.append("--debug") if flags: diff --git a/docs/installation.rst b/docs/installation.rst index 514d20e74..6720d2dce 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -380,28 +380,28 @@ Build Options using a setting of 1. By default, it uses 4 CPUs, or if 4 are not available, as many as are present. -* Config settings: ``-C disable=zlib``, ``-C disable=jpeg``, - ``-C disable=tiff``, ``-C disable=freetype``, ``-C disable=raqm``, - ``-C disable=lcms``, ``-C disable=webp``, ``-C disable=webpmux``, - ``-C disable=jpeg2000``, ``-C disable=imagequant``, ``-C disable=xcb``. +* Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, + ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. -* Config settings: ``-C enable=zlib``, ``-C enable=jpeg``, - ``-C enable=tiff``, ``-C enable=freetype``, ``-C enable=raqm``, - ``-C enable=lcms``, ``-C enable=webp``, ``-C enable=webpmux``, - ``-C enable=jpeg2000``, ``-C enable=imagequant``, ``-C enable=xcb``. +* Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, + ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. Require that the corresponding feature is built. The build will raise an exception if the libraries are not found. Webpmux (WebP metadata) relies on WebP support. Tcl and Tk also must be used together. -* Config settings: ``-C vendor=raqm``, ``-C vendor=fribidi``. +* Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and a shim that dynamically loads libfribidi at runtime. These are used to compile the standard Pillow wheels. Compiling libraqm requires a C99-compliant compiler. -* Build flag: ``-C disable=platform-guessing``. Skips all of the +* Build flag: ``-C platform-guessing=disable``. Skips all of the platform dependent guessing of include and library directories for automated build systems that configure the proper paths in the environment variables (e.g. Buildroot). @@ -413,7 +413,7 @@ Build Options Sample usage:: - python3 -m pip install --upgrade Pillow -C enable=[feature] + python3 -m pip install --upgrade Pillow -C [feature]=enable Platform Support ---------------- From e45da2ae17e79cd2e02bc894f3c825f9126101a0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 11:07:09 +1000 Subject: [PATCH 351/512] Do not close provided file handles with libtiff --- src/PIL/TiffImagePlugin.py | 10 +--------- src/libImaging/TiffDecode.c | 6 +++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 1ca1b6ea9..347678642 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1253,9 +1253,8 @@ class TiffImageFile(ImageFile.ImageFile): # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. - # libtiff closes the file descriptor, so pass in a dup. try: - fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno()) + fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ # should also eliminate the need for fp.tell # in _seek @@ -1305,18 +1304,11 @@ class TiffImageFile(ImageFile.ImageFile): # UNDONE -- so much for that buffer size thing. n, err = decoder.decode(self.fp.read()) - if fp: - try: - os.close(fp) - except OSError: - pass - self.tile = [] self.readonly = 0 self.load_end() - # libtiff closed the fp in a, we need to close self.fp, if possible if close_self_fp: self.fp.close() self.fp = None # might be shared diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 428cd93d2..9361de834 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -720,7 +720,11 @@ ImagingLibTiffDecode( } decode_err: - TIFFClose(tiff); + if (clientstate->fp) { + TIFFCleanup(tiff); + } else { + TIFFClose(tiff); + } TRACE(("Done Decoding, Returning \n")); // Returning -1 here to force ImageFile.load to break, rather than // even think about looping back around. From 0835be95cbbdc1beaac0dfaccd7a358621f619bf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 15:07:11 +1000 Subject: [PATCH 352/512] Added comment --- src/libImaging/TiffDecode.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c index 9361de834..35122f182 100644 --- a/src/libImaging/TiffDecode.c +++ b/src/libImaging/TiffDecode.c @@ -720,9 +720,14 @@ ImagingLibTiffDecode( } decode_err: + // TIFFClose in libtiff calls tif_closeproc and TIFFCleanup if (clientstate->fp) { + // Pillow will manage the closing of the file rather than libtiff + // So only call TIFFCleanup TIFFCleanup(tiff); } else { + // When tif_closeproc refers to our custom _tiffCloseProc though, + // that is fine, as it does not close the file TIFFClose(tiff); } TRACE(("Done Decoding, Returning \n")); From 97bd53392ce136617ead36c11d50def9d32ab3e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Jun 2023 18:36:41 +1000 Subject: [PATCH 353/512] Do not use temporary file when grabbing clipboard on Linux --- src/PIL/ImageGrab.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 7f6d50af4..39ecdf420 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -15,6 +15,7 @@ # See the README file for information on usage and redistribution. # +import io import os import shutil import subprocess @@ -128,8 +129,6 @@ def grabclipboard(): files = data[o:].decode("mbcs").split("\0") return files[: files.index("")] if isinstance(data, bytes): - import io - data = io.BytesIO(data) if fmt == "png": from . import PngImagePlugin @@ -159,13 +158,12 @@ def grabclipboard(): else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) - fh, filepath = tempfile.mkstemp() - err = subprocess.run(args, stdout=fh, stderr=subprocess.PIPE).stderr - os.close(fh) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + err = p.stderr if err: msg = f"{args[0]} error: {err.strip().decode()}" raise ChildProcessError(msg) - im = Image.open(filepath) + data = io.BytesIO(p.stdout) + im = Image.open(data) im.load() - os.unlink(filepath) return im From 3b65261c966648e5d4f87cd49bb12cba5345547d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 17:54:55 +1000 Subject: [PATCH 354/512] Remove temporary file when error is raised --- src/PIL/EpsImagePlugin.py | 7 +++++++ src/PIL/JpegImagePlugin.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 1c88d22c7..bdac874c4 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -134,6 +134,13 @@ def Ghostscript(tile, size, fp, scale=1, transparency=False): if gs_windows_binary is not None: if not gs_windows_binary: + try: + os.unlink(outfile) + if infile_temp: + os.unlink(infile_temp) + except OSError: + pass + msg = "Unable to locate Ghostscript on paths" raise OSError(msg) command[0] = gs_windows_binary diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 5dd1a61af..dfc7e6e9f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -457,6 +457,11 @@ class JpegImageFile(ImageFile.ImageFile): if os.path.exists(self.filename): subprocess.check_call(["djpeg", "-outfile", path, self.filename]) else: + try: + os.unlink(path) + except OSError: + pass + msg = "Invalid Filename" raise ValueError(msg) From 97df237dc81c930d983b4025b7b3a97d043dfd7c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 18:04:39 +1000 Subject: [PATCH 355/512] Moved test into separate function --- Tests/test_file_apng.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index c62231cd4..a22ac581d 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -440,12 +440,6 @@ def test_apng_save_duration_loop(tmp_path): assert im.n_frames == 1 assert im.info.get("duration") == 750 - # test removal of duplicated frames with a single duration - frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) - with Image.open(test_file) as im: - assert im.n_frames == 1 - assert im.info.get("duration") == 1500 - # test info duration frame.info["duration"] = 750 frame.save(test_file, save_all=True) @@ -453,6 +447,17 @@ def test_apng_save_duration_loop(tmp_path): assert im.info.get("duration") == 750 +def test_apng_save_duplicate_duration(tmp_path): + test_file = str(tmp_path / "temp.png") + frame = Image.new("RGB", (1, 1)) + + # Test a single duration is correctly combined across duplicate frames + frame.save(test_file, save_all=True, append_images=[frame, frame], duration=500) + with Image.open(test_file) as im: + assert im.n_frames == 1 + assert im.info.get("duration") == 1500 + + def test_apng_save_disposal(tmp_path): test_file = str(tmp_path / "temp.png") size = (128, 64) From 7c533276f28518ec2825e1cae3f0df427b5c565b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Jun 2023 19:53:50 +1000 Subject: [PATCH 356/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 190751ad2..c51f8fb94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,27 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed combining single duration across duplicate APNG frames #7146 + [radarhere] + +- Remove temporary file when error is raised #7148 + [radarhere] + +- Do not use temporary file when grabbing clipboard on Linux #7200 + [radarhere] + +- If the clipboard fails to open on Windows, wait and try again #7141 + [radarhere] + +- Fixed saving multiple 1 mode frames to GIF #7181 + [radarhere] + +- Replaced absolute PIL import with relative import #7173 + [radarhere] + +- Replaced deprecated Py_FileSystemDefaultEncoding for Python >= 3.12 #7192 + [radarhere] + - Improved wl-paste mimetype handling in ImageGrab #7094 [rrcgat, radarhere] From 15edb6d625f94e0f7e9047ab76ed08762ab2f53a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Jun 2023 22:33:55 +1000 Subject: [PATCH 357/512] Fixed signedness comparison warning --- src/libImaging/Jpeg2KEncode.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index 0d7e896b7..de8586706 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -464,7 +464,7 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { } if (!context->num_resolutions) { - while (tile_width < (1 << (params.numresolution - 1U)) || tile_height < (1 << (params.numresolution - 1U))) { + while (tile_width < (1U << (params.numresolution - 1U)) || tile_height < (1U << (params.numresolution - 1U))) { params.numresolution -= 1; } } From da6b2ec28506a132fad9674e1badb1624aed7a8b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 9 Jun 2023 10:47:20 +1000 Subject: [PATCH 358/512] Document order of kernel weights --- src/PIL/ImageFilter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 63d6dcf5c..33bc7cc2e 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -35,7 +35,7 @@ class BuiltinFilter(MultibandFilter): class Kernel(BuiltinFilter): """ - Create a convolution kernel. The current version only + Create a convolution kernel. The current version only supports 3x3 and 5x5 integer and floating point kernels. In the current version, kernels can only be applied to @@ -43,9 +43,10 @@ class Kernel(BuiltinFilter): :param size: Kernel size, given as (width, height). In the current version, this must be (3,3) or (5,5). - :param kernel: A sequence containing kernel weights. + :param kernel: A sequence containing kernel weights. The kernel will + be flipped vertically before being applied to the image. :param scale: Scale factor. If given, the result for each pixel is - divided by this value. The default is the sum of the + divided by this value. The default is the sum of the kernel weights. :param offset: Offset. If given, this value is added to the result, after it has been divided by the scale factor. From 748a4d0fcd517e0e6e86ae15f4be9b0bcf65747d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 14:26:28 +1000 Subject: [PATCH 359/512] Removed unused variable --- src/libImaging/Storage.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libImaging/Storage.c b/src/libImaging/Storage.c index 7cf00ef35..128595f65 100644 --- a/src/libImaging/Storage.c +++ b/src/libImaging/Storage.c @@ -37,8 +37,6 @@ #include "Imaging.h" #include -int ImagingNewCount = 0; - /* -------------------------------------------------------------------- * Standard image object. */ From aeb6e9909e94d1ad6c86ebf04a6db6cd77e016a3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 15:57:05 +1000 Subject: [PATCH 360/512] Removed unused argument --- src/PIL/Image.py | 2 +- src/_imaging.c | 5 ++--- src/libImaging/Filter.c | 2 +- src/libImaging/Imaging.h | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e0fb6a885..fa70f674b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1254,7 +1254,7 @@ class Image: if ymargin is None: ymargin = xmargin self.load() - return self._new(self.im.expand(xmargin, ymargin, 0)) + return self._new(self.im.expand(xmargin, ymargin)) def filter(self, filter): """ diff --git a/src/_imaging.c b/src/_imaging.c index 281f3a4d2..5c6380fee 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1027,12 +1027,11 @@ _crop(ImagingObject *self, PyObject *args) { static PyObject * _expand_image(ImagingObject *self, PyObject *args) { int x, y; - int mode = 0; - if (!PyArg_ParseTuple(args, "ii|i", &x, &y, &mode)) { + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { return NULL; } - return PyImagingNew(ImagingExpand(self->image, x, y, mode)); + return PyImagingNew(ImagingExpand(self->image, x, y)); } static PyObject * diff --git a/src/libImaging/Filter.c b/src/libImaging/Filter.c index 4b8d2bf05..4dcd368ca 100644 --- a/src/libImaging/Filter.c +++ b/src/libImaging/Filter.c @@ -49,7 +49,7 @@ clip32(float in) { } Imaging -ImagingExpand(Imaging imIn, int xmargin, int ymargin, int mode) { +ImagingExpand(Imaging imIn, int xmargin, int ymargin) { Imaging imOut; int x, y; ImagingSectionCookie cookie; diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index d9ded1852..beec8a8f2 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -290,7 +290,7 @@ ImagingConvertTransparent(Imaging im, const char *mode, int r, int g, int b); extern Imaging ImagingCrop(Imaging im, int x0, int y0, int x1, int y1); extern Imaging -ImagingExpand(Imaging im, int x, int y, int mode); +ImagingExpand(Imaging im, int x, int y); extern Imaging ImagingFill(Imaging im, const void *ink); extern int From 389ad11693deb5ea39b8edf0eb47263582a3f5f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 17:10:42 +1000 Subject: [PATCH 361/512] Only call text_layout once in getmask2 --- src/PIL/ImageFont.py | 34 +++---- src/_imagingft.c | 215 ++++++++++++++++++++++++++----------------- 2 files changed, 144 insertions(+), 105 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..7b4ca5814 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -26,7 +26,6 @@ # import base64 -import math import os import sys import warnings @@ -551,28 +550,23 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ - size, offset = self.font.getsize( - text, mode, direction, features, language, anchor - ) if start is None: start = (0, 0) - size = tuple(math.ceil(size[i] + stroke_width * 2 + start[i]) for i in range(2)) - offset = offset[0] - stroke_width, offset[1] - stroke_width + im, size, offset = self.font.render( + text, + Image.core.fill, + mode, + direction, + features, + language, + stroke_width, + anchor, + ink, + start[0], + start[1], + Image.MAX_IMAGE_PIXELS, + ) Image._decompression_bomb_check(size) - im = Image.core.fill("RGBA" if mode == "RGBA" else "L", size, 0) - if min(size): - self.font.render( - text, - im.id, - mode, - direction, - features, - language, - stroke_width, - ink, - start[0], - start[1], - ) return im, offset def font_variant( diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..95f12eb5a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -551,73 +551,25 @@ font_getlength(FontObject *self, PyObject *args) { return PyLong_FromLong(length); } -static PyObject * -font_getsize(FontObject *self, PyObject *args) { +static int +bounding_box_and_anchors(FT_Face face, const char *anchor, int horizontal_dir, GlyphInfo *glyph_info, size_t count, int load_flags, int *width, int *height, int *x_offset, int *y_offset) { int position; /* pen position along primary axis, in 26.6 precision */ int advanced; /* pen position along primary axis, in pixels */ int px, py; /* position of current glyph, in pixels */ int x_min, x_max, y_min, y_max; /* text bounding box, in pixels */ int x_anchor, y_anchor; /* offset of point drawn at (0, 0), in pixels */ - int load_flags; /* FreeType load_flags parameter */ int error; - FT_Face face; FT_Glyph glyph; - FT_BBox bbox; /* glyph bounding box */ - GlyphInfo *glyph_info = NULL; /* computed text layout */ - size_t i, count; /* glyph_info index and length */ - int horizontal_dir; /* is primary axis horizontal? */ - int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ - int color = 0; /* is FT_LOAD_COLOR enabled? */ - const char *mode = NULL; - const char *dir = NULL; - const char *lang = NULL; - const char *anchor = NULL; - PyObject *features = Py_None; - PyObject *string; - - /* calculate size and bearing for a given string */ - - if (!PyArg_ParseTuple( - args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { - return NULL; - } - - horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; - - mask = mode && strcmp(mode, "1") == 0; - color = mode && strcmp(mode, "RGBA") == 0; - - if (anchor == NULL) { - anchor = horizontal_dir ? "la" : "lt"; - } - if (strlen(anchor) != 2) { - goto bad_anchor; - } - - count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); - if (PyErr_Occurred()) { - return NULL; - } - - load_flags = FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - + FT_BBox bbox; /* glyph bounding box */ + size_t i; /* glyph_info index */ /* * text bounds are given by: * - bounding boxes of individual glyphs * - pen line, i.e. 0 to `advanced` along primary axis * this means point (0, 0) is part of the text bounding box */ - face = NULL; position = x_min = x_max = y_min = y_max = 0; for (i = 0; i < count; i++) { - face = self->face; - if (horizontal_dir) { px = PIXEL(position + glyph_info[i].x_offset); py = PIXEL(glyph_info[i].y_offset); @@ -640,12 +592,14 @@ font_getsize(FontObject *self, PyObject *args) { error = FT_Load_Glyph(face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + return 1; } error = FT_Get_Glyph(face->glyph, &glyph); if (error) { - return geterror(error); + geterror(error); + return 1; } FT_Glyph_Get_CBox(glyph, FT_GLYPH_BBOX_PIXELS, &bbox); @@ -669,13 +623,15 @@ font_getsize(FontObject *self, PyObject *args) { FT_Done_Glyph(glyph); } - if (glyph_info) { - PyMem_Free(glyph_info); - glyph_info = NULL; + if (anchor == NULL) { + anchor = horizontal_dir ? "la" : "lt"; + } + if (strlen(anchor) != 2) { + goto bad_anchor; } x_anchor = y_anchor = 0; - if (face) { + if (count) { if (horizontal_dir) { switch (anchor[0]) { case 'l': // left @@ -693,15 +649,15 @@ font_getsize(FontObject *self, PyObject *args) { } switch (anchor[1]) { case 'a': // ascender - y_anchor = PIXEL(self->face->size->metrics.ascender); + y_anchor = PIXEL(face->size->metrics.ascender); break; case 't': // top y_anchor = y_max; break; case 'm': // middle (ascender + descender) / 2 y_anchor = PIXEL( - (self->face->size->metrics.ascender + - self->face->size->metrics.descender) / + (face->size->metrics.ascender + + face->size->metrics.descender) / 2); break; case 's': // horizontal baseline @@ -711,7 +667,7 @@ font_getsize(FontObject *self, PyObject *args) { y_anchor = y_min; break; case 'd': // descender - y_anchor = PIXEL(self->face->size->metrics.descender); + y_anchor = PIXEL(face->size->metrics.descender); break; default: goto bad_anchor; @@ -751,17 +707,74 @@ font_getsize(FontObject *self, PyObject *args) { } } } - - return Py_BuildValue( - "(ii)(ii)", - (x_max - x_min), - (y_max - y_min), - (-x_anchor + x_min), - -(-y_anchor + y_max)); + *width = x_max - x_min; + *height = y_max - y_min; + *x_offset = -x_anchor + x_min; + *y_offset = -(-y_anchor + y_max); + return 0; bad_anchor: PyErr_Format(PyExc_ValueError, "bad anchor specified: %s", anchor); - return NULL; + return 1; +} + +static PyObject * +font_getsize(FontObject *self, PyObject *args) { + int width, height, x_offset, y_offset; + int load_flags; /* FreeType load_flags parameter */ + int error; + GlyphInfo *glyph_info = NULL; /* computed text layout */ + size_t count; /* glyph_info length */ + int horizontal_dir; /* is primary axis horizontal? */ + int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ + int color = 0; /* is FT_LOAD_COLOR enabled? */ + const char *mode = NULL; + const char *dir = NULL; + const char *lang = NULL; + const char *anchor = NULL; + PyObject *features = Py_None; + PyObject *string; + + /* calculate size and bearing for a given string */ + + if (!PyArg_ParseTuple( + args, "O|zzOzz:getsize", &string, &mode, &dir, &features, &lang, &anchor)) { + return NULL; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + mask = mode && strcmp(mode, "1") == 0; + color = mode && strcmp(mode, "RGBA") == 0; + + count = text_layout(string, self, dir, features, lang, &glyph_info, mask, color); + if (PyErr_Occurred()) { + return NULL; + } + + load_flags = FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (glyph_info) { + PyMem_Free(glyph_info); + glyph_info = NULL; + } + if (error) { + return NULL; + } + + return Py_BuildValue( + "(ii)(ii)", + width, + height, + x_offset, + y_offset); } static PyObject * @@ -785,6 +798,7 @@ font_render(FontObject *self, PyObject *args) { unsigned int bitmap_y; /* glyph bitmap y index */ unsigned char *source; /* glyph bitmap source buffer */ unsigned char convert_scale; /* scale factor for non-8bpp bitmaps */ + PyObject *image; Imaging im; Py_ssize_t id; int mask = 0; /* is FT_LOAD_TARGET_MONO enabled? */ @@ -795,27 +809,34 @@ font_render(FontObject *self, PyObject *args) { const char *mode = NULL; const char *dir = NULL; const char *lang = NULL; + const char *anchor = NULL; PyObject *features = Py_None; PyObject *string; + PyObject *fill; float x_start = 0; float y_start = 0; + int width, height, x_offset, y_offset; + int horizontal_dir; /* is primary axis horizontal? */ + PyObject *max_image_pixels = Py_None; /* render string into given buffer (the buffer *must* have the right size, or this will crash) */ if (!PyArg_ParseTuple( args, - "On|zzOziLff:render", + "OO|zzOzizLffO:render", &string, - &id, + &fill, &mode, &dir, &features, &lang, &stroke_width, + &anchor, &foreground_ink_long, &x_start, - &y_start)) { + &y_start, + &max_image_pixels)) { return NULL; } @@ -841,8 +862,41 @@ font_render(FontObject *self, PyObject *args) { if (PyErr_Occurred()) { return NULL; } - if (count == 0) { - Py_RETURN_NONE; + + load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; + if (mask) { + load_flags |= FT_LOAD_TARGET_MONO; + } + if (color) { + load_flags |= FT_LOAD_COLOR; + } + + horizontal_dir = dir && strcmp(dir, "ttb") == 0 ? 0 : 1; + + error = bounding_box_and_anchors(self->face, anchor, horizontal_dir, glyph_info, count, load_flags, &width, &height, &x_offset, &y_offset); + if (error) { + PyMem_Del(glyph_info); + return NULL; + } + + width += stroke_width * 2 + ceil(x_start); + height += stroke_width * 2 + ceil(y_start); + if (max_image_pixels != Py_None) { + if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + } + } + + image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); + im = (Imaging)id; + + x_offset -= stroke_width; + y_offset -= stroke_width; + if (count == 0 || width == 0 || height == 0) { + PyMem_Del(glyph_info); + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); } if (stroke_width) { @@ -859,15 +913,6 @@ font_render(FontObject *self, PyObject *args) { 0); } - im = (Imaging)id; - load_flags = stroke_width ? FT_LOAD_NO_BITMAP : FT_LOAD_DEFAULT; - if (mask) { - load_flags |= FT_LOAD_TARGET_MONO; - } - if (color) { - load_flags |= FT_LOAD_COLOR; - } - /* * calculate x_min and y_max * must match font_getsize or there may be clipping! @@ -1064,7 +1109,7 @@ font_render(FontObject *self, PyObject *args) { } FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - Py_RETURN_NONE; + return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: if (stroker != NULL) { From 4dcca33d3099e29110b24cc507e8f0799e1d1ab7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:06:25 +1000 Subject: [PATCH 362/512] Removed unused arguments --- src/_imagingft.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 80f862bb7..8fc1fa7d0 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -254,9 +254,7 @@ text_layout_raqm( const char *dir, PyObject *features, const char *lang, - GlyphInfo **glyph_info, - int mask, - int color) { + GlyphInfo **glyph_info) { size_t i = 0, count = 0, start = 0; raqm_t *rq; raqm_glyph_t *glyphs = NULL; @@ -493,7 +491,7 @@ text_layout( #ifdef HAVE_RAQM if (have_raqm && self->layout_engine == LAYOUT_RAQM) { count = text_layout_raqm( - string, self, dir, features, lang, glyph_info, mask, color); + string, self, dir, features, lang, glyph_info); } else #endif { From 16d82c2dfd473836e7903165917584bf938b0345 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 19:37:54 +1000 Subject: [PATCH 363/512] Improved coverage --- Tests/test_imagefont.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7ea485a55..4a40d1d1d 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -463,6 +463,11 @@ def test_default_font(): assert_image_equal_tofile(im, "Tests/images/default_font.png") +@pytest.mark.parametrize("mode", (None, "1", "RGBA")) +def test_getbbox(font, mode): + assert (0, 4, 12, 16) == font.getbbox("A", mode) + + def test_getbbox_empty(font): # issue #2614, should not crash. assert (0, 0, 0, 0) == font.getbbox("") From 1756df461561234184611dfe8b42f1b7f33de1f0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Jun 2023 20:24:34 +1000 Subject: [PATCH 364/512] Removed unused private method --- src/PIL/ImageFont.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index ea4549cf5..abcb88520 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -226,10 +226,6 @@ class FreeTypeFont: path, size, index, encoding, layout_engine = state self.__init__(path, size, index, encoding, layout_engine) - def _multiline_split(self, text): - split_character = "\n" if isinstance(text, str) else b"\n" - return text.split(split_character) - def getname(self): """ :return: A tuple of the font family (e.g. Helvetica) and the font style From 5a0fb8ec127b7b23d13d22d7a8ade5505835435f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 11 Jun 2023 00:05:47 +0300 Subject: [PATCH 365/512] Add Debian 12 Bookworm --- .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 4f01abe44..3bcb8cfbc 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-x86, + debian-12-bookworm-x86, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/docs/installation.rst b/docs/installation.rst index ad27b67ee..ac54b037d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -448,6 +448,8 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86 | +----------------------------------+----------------------------+---------------------+ +| Debian 12 Bookworm | 3.11 | x86 | ++----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 38 | 3.11 | x86-64 | From c24c1ccf8163c90dfbc3110490846d3113d3418a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:52:55 +1000 Subject: [PATCH 366/512] Use "not in" Co-authored-by: Aarni Koskela --- src/PIL/GdImageFile.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/features.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7dda4f143..bafc43a19 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -47,7 +47,7 @@ class GdImageFile(ImageFile.ImageFile): # Header s = self.fp.read(1037) - if not i16(s) in [65534, 65535]: + if i16(s) not in [65534, 65535]: msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fa70f674b..66b0f0e06 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1731,7 +1731,7 @@ class Image: if not isinstance(dest, (list, tuple)): msg = "Destination must be a tuple" raise ValueError(msg) - if not len(source) in (2, 4): + if len(source) not in (2, 4): msg = "Source must be a 2 or 4-tuple" raise ValueError(msg) if not len(dest) == 2: diff --git a/src/PIL/features.py b/src/PIL/features.py index 80a16a75e..f14e60cf5 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -24,7 +24,7 @@ def check_module(feature): :returns: ``True`` if available, ``False`` otherwise. :raises ValueError: If the module is not defined in this version of Pillow. """ - if not (feature in modules): + if feature not in modules: msg = f"Unknown module {feature}" raise ValueError(msg) From 538971532da065ada6528d441ea39693e108ebf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 08:55:21 +1000 Subject: [PATCH 367/512] Corrected error code Co-authored-by: nulano --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index 25a4d3517..dbea673f9 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -189,7 +189,7 @@ getfont(PyObject *self_, PyObject *args, PyObject *kw) { /* Don't free this before FT_Done_Face */ self->font_bytes = PyMem_Malloc(font_bytes_size); if (!self->font_bytes) { - error = 65; // Out of Memory in Freetype. + error = FT_Err_Out_Of_Memory; } if (!error) { memcpy(self->font_bytes, font_bytes, (size_t)font_bytes_size); From f338f35657baad30295b45353d699cace83d3699 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 10:01:36 +1000 Subject: [PATCH 368/512] Changed inPlace to be keyword-only argument --- src/PIL/ImageOps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index facc30ba0..752d132a3 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, inPlace=False): +def exif_transpose(image, *, inPlace=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. + :param inPlace: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the From 187e9a46af160b6510c0262ef1f52e4c35ce579e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:21:43 +1000 Subject: [PATCH 369/512] Improved documention of "corners" argument for rounded_rectangle --- docs/reference/ImageDraw.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 524f821fb..31f63695e 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -328,7 +328,7 @@ Methods .. versionadded:: 5.3.0 -.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1) +.. py:method:: ImageDraw.rounded_rectangle(xy, radius=0, fill=None, outline=None, width=1, corners=None) Draws a rounded rectangle. @@ -341,6 +341,7 @@ Methods :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)``. + Keyword-only argument. .. versionadded:: 8.2.0 From bae918280d8bd74d2ace512a71eabc76e05a1d0f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:25:12 +1000 Subject: [PATCH 370/512] Changed alpha_only to keyword-only argument Co-authored-by: Hugo van Kemenade --- 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 f5d120671..74c1bd7f6 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1279,7 +1279,7 @@ class Image: """ return ImageMode.getmode(self.mode).bands - def getbbox(self, alpha_only=True): + def getbbox(self, *, alpha_only=True): """ Calculates the bounding box of the non-zero regions in the image. From d7c7b832f142aa5a66ee5dd9ad1ce4f7e2afa241 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:25:42 +1000 Subject: [PATCH 371/512] Highlight code Co-authored-by: Hugo van Kemenade --- 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 74c1bd7f6..8eebd28f9 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1284,8 +1284,8 @@ class Image: Calculates the bounding box of the non-zero regions in the image. - :param alpha_only: Optional flag, defaulting to true. - If true and the image has an alpha channel, trim transparent pixels. + :param alpha_only: Optional flag, defaulting to ``True``. + If ``True`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See From 044de40c93064aa938dc45868ce52698062009da Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:28:14 +1000 Subject: [PATCH 372/512] Document that alpha_only is a keyword-only argument --- src/PIL/Image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4413bae28..340ba4e3b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1301,6 +1301,7 @@ class Image: :param alpha_only: Optional flag, defaulting to ``True``. If ``True`` and the image has an alpha channel, trim transparent pixels. Otherwise, trim pixels when all channels are zero. + Keyword-only argument. :returns: The bounding box is returned as a 4-tuple defining the left, upper, right, and lower pixel coordinate. See :ref:`coordinate-system`. If the image is completely empty, this From 119a0dfb0113391bbab34fcd7be94457877506cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 11:29:22 +1000 Subject: [PATCH 373/512] Updated tests now that alpha_only is keyword-only --- Tests/test_image_getbbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image_getbbox.py b/Tests/test_image_getbbox.py index eec978210..afca66703 100644 --- a/Tests/test_image_getbbox.py +++ b/Tests/test_image_getbbox.py @@ -45,11 +45,11 @@ def test_bbox(): @pytest.mark.parametrize("mode", ("RGBA", "RGBa", "La", "LA", "PA")) def test_bbox_alpha_only_false(mode): im = Image.new(mode, (100, 100)) - assert im.getbbox(False) is None + assert im.getbbox(alpha_only=False) is None fill_color = [1] * Image.getmodebands(mode) fill_color[-1] = 0 im.paste(tuple(fill_color), (25, 25, 75, 75)) - assert im.getbbox(False) == (25, 25, 75, 75) + assert im.getbbox(alpha_only=False) == (25, 25, 75, 75) assert im.getbbox() is None From 541d2605b9235a427379e3df51c9cdd4ffe59998 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 14:21:07 +1000 Subject: [PATCH 374/512] Allow alpha differences to indicate different frames when saving GIF --- Tests/test_file_gif.py | 12 ++++++++++++ src/PIL/GifImagePlugin.py | 4 ++-- src/PIL/PngImagePlugin.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 0e50ee1ab..f4a17264f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1130,6 +1130,18 @@ def test_bbox(tmp_path): assert reread.n_frames == 2 +def test_bbox_alpha(tmp_path): + out = str(tmp_path / "temp.gif") + + im = Image.new("RGBA", (1, 2), (255, 0, 0, 255)) + im.putpixel((0, 1), (255, 0, 0, 0)) + im2 = Image.new("RGBA", (1, 2), (255, 0, 0, 0)) + im.save(out, save_all=True, append_images=[im2]) + + with Image.open(out) as reread: + assert reread.n_frames == 2 + + def test_palette_save_L(tmp_path): # Generate an L mode image with a separate palette diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 2f92e9467..cf2993e38 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -569,9 +569,9 @@ def _getbbox(base_im, im_frame): delta = ImageChops.subtract_modulo(im_frame, base_im) else: delta = ImageChops.subtract_modulo( - im_frame.convert("RGB"), base_im.convert("RGB") + im_frame.convert("RGBA"), base_im.convert("RGBA") ) - return delta.getbbox() + return delta.getbbox(alpha_only=False) def _write_multiple_frames(im, fp, palette): diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index ea3052fbf..bfa8cb7ac 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1140,7 +1140,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images) delta = ImageChops.subtract_modulo( im_frame.convert("RGBA"), base_im.convert("RGBA") ) - bbox = delta.im.getbbox(False) + bbox = delta.getbbox(alpha_only=False) if ( not bbox and prev_disposal == encoderinfo.get("disposal") From 38d63868bf395ac1eb4869bc415de424f0863e45 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 15:46:24 +1000 Subject: [PATCH 375/512] Do not import internal class --- _custom_build/backend.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/_custom_build/backend.py b/_custom_build/backend.py index 86fe60817..9b3265a94 100755 --- a/_custom_build/backend.py +++ b/_custom_build/backend.py @@ -1,10 +1,12 @@ import sys from setuptools.build_meta import * # noqa: F401, F403 -from setuptools.build_meta import _BuildMetaBackend +from setuptools.build_meta import build_wheel + +backend_class = build_wheel.__self__.__class__ -class _CustomBuildMetaBackend(_BuildMetaBackend): +class _CustomBuildMetaBackend(backend_class): def run_setup(self, setup_script="setup.py"): if self.config_settings: From 7d97fa8b86a9f61069579fbedb2cc09fb437b12f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 16:12:47 +1000 Subject: [PATCH 376/512] Use snake case --- Tests/test_imageops.py | 4 ++-- src/PIL/ImageOps.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e7d04cceb..b05785be0 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -404,13 +404,13 @@ def test_exif_transpose(): assert 0x0112 not in transposed_im.getexif() -def test_exif_transpose_inplace(): +def test_exif_transpose_in_place(): with Image.open("Tests/images/orientation_rectangle.jpg") as im: assert im.size == (2, 1) assert im.getexif()[0x0112] == 8 expected = im.rotate(90, expand=True) - ImageOps.exif_transpose(im, inPlace=True) + ImageOps.exif_transpose(im, in_place=True) assert im.size == (1, 2) assert 0x0112 not in im.getexif() assert_image_equal(im, expected) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 752d132a3..17702778c 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -576,13 +576,13 @@ def solarize(image, threshold=128): return _lut(image, lut) -def exif_transpose(image, *, inPlace=False): +def exif_transpose(image, *, in_place=False): """ If an image has an EXIF Orientation tag, other than 1, transpose the image accordingly, and remove the orientation data. :param image: The image to transpose. - :param inPlace: Boolean. Keyword-only argument. + :param in_place: Boolean. Keyword-only argument. If ``True``, the original image is modified in-place, and ``None`` is returned. If ``False`` (default), a new :py:class:`~PIL.Image.Image` object is returned with the transposition applied. If there is no transposition, a copy of the @@ -601,11 +601,11 @@ def exif_transpose(image, *, inPlace=False): }.get(orientation) if method is not None: transposed_image = image.transpose(method) - if inPlace: + if in_place: image.im = transposed_image.im image.pyaccess = None image._size = transposed_image._size - exif_image = image if inPlace else transposed_image + exif_image = image if in_place else transposed_image exif = exif_image.getexif() if ExifTags.Base.Orientation in exif: @@ -622,7 +622,7 @@ def exif_transpose(image, *, inPlace=False): exif_image.info["XML:com.adobe.xmp"] = re.sub( pattern, "", exif_image.info["XML:com.adobe.xmp"] ) - if not inPlace: + if not in_place: return transposed_image - elif not inPlace: + elif not in_place: return image.copy() From b2b05f3b83ade2065b98d310214e6f49f04f57f5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 18:55:19 +1000 Subject: [PATCH 377/512] Moved QOI from Write-Only to Read-Only --- docs/handbook/image-file-formats.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 74ba883b1..bbcf48e42 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1380,6 +1380,12 @@ PSD Pillow identifies and reads PSD files written by Adobe Photoshop 2.5 and 3.0. +QOI +^^^ + +.. versionadded:: 9.5.0 + +Pillow identifies and reads images in Quite OK Image format. SUN ^^^ @@ -1562,13 +1568,6 @@ 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 images in Quite OK Image format. - XV Thumbnails ^^^^^^^^^^^^^ From 594fbf79b8a30a9fd3be1171038b5d787f4d00cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Jun 2023 23:01:45 +1000 Subject: [PATCH 378/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c51f8fb94..4af3fa516 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added in_place argument to ImageOps.exif_transpose() #7092 + [radarhere] + +- Fixed calling putpalette() on L and LA images before load() #7187 + [radarhere] + +- Fixed saving TIFF multiframe images with LONG8 tag types #7078 + [radarhere] + - Fixed combining single duration across duplicate APNG frames #7146 [radarhere] From 618c00c4ea64ddd40f3485db549f1549f7f27b04 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 14:27:33 +1000 Subject: [PATCH 379/512] Return early if image is null --- src/_imagingft.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_imagingft.c b/src/_imagingft.c index dbea673f9..d4422a43d 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -887,6 +887,10 @@ font_render(FontObject *self, PyObject *args) { } image = PyObject_CallFunction(fill, "s(ii)", strcmp(mode, "RGBA") == 0 ? "RGBA" : "L", width, height); + if (image == NULL) { + PyMem_Del(glyph_info); + return NULL; + } id = PyLong_AsSsize_t(PyObject_GetAttrString(image, "id")); im = (Imaging)id; From 98cc2e63ac9c78e57476d563a70a27397fea87e6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Jun 2023 12:52:57 +1000 Subject: [PATCH 380/512] Destroy image on error --- src/_imagingft.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d4422a43d..02d54fe23 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -904,7 +904,8 @@ font_render(FontObject *self, PyObject *args) { if (stroke_width) { error = FT_Stroker_New(library, &stroker); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } FT_Stroker_Set( @@ -927,7 +928,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags | FT_LOAD_RENDER); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -958,7 +960,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Load_Glyph(self->face, glyph_info[i].index, load_flags); if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } glyph_slot = self->face->glyph; @@ -972,7 +975,8 @@ font_render(FontObject *self, PyObject *args) { error = FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_NORMAL, &origin, 1); } if (error) { - return geterror(error); + geterror(error); + goto glyph_error; } bitmap_glyph = (FT_BitmapGlyph)glyph; @@ -1114,6 +1118,12 @@ font_render(FontObject *self, PyObject *args) { return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); glyph_error: + if (im->destroy) { + im->destroy(im); + } + if (im->image) { + free(im->image); + } if (stroker != NULL) { FT_Done_Glyph(glyph); } From 43b693972a4b2f5ffe00ecb21ec9cc46ab7a3352 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 00:25:59 +1000 Subject: [PATCH 381/512] Added PyPy 3.10 and removed PyPy 3.8 --- .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 076b80839..cab47b01f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -28,10 +28,10 @@ jobs: architecture: ["x86", "x64"] include: # PyPy 7.3.4+ only ships 64-bit binaries for Windows - - python-version: "pypy3.8" - architecture: "x64" - python-version: "pypy3.9" architecture: "x64" + - python-version: "pypy3.10" + architecture: "x64" timeout-minutes: 30 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afb8fb56c..893c0d12c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,8 +29,8 @@ jobs: "ubuntu-latest", ] python-version: [ + "pypy3.10", "pypy3.9", - "pypy3.8", "3.12-dev", "3.11", "3.10", From 7044038e701fd777bf2c7dc38b02c4d944b086ba Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 14:35:44 +1000 Subject: [PATCH 382/512] Fixed decompression bomb check --- ...om-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf | Bin 0 -> 30 bytes Tests/test_imagefont.py | 1 + src/_imagingft.c | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf diff --git a/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf b/Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe200842e41daa66ddb9a3ea5137726681bc2d2f GIT binary patch literal 30 kcmZQ%U}NO`&A`CG$jHcNpjXjl(8$C7=N4O!AW)0}08Ie}y8r+H literal 0 HcmV?d00001 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 4a40d1d1d..7fa8ff8cb 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1042,6 +1042,7 @@ def test_render_mono_size(): "test_file", [ "Tests/fonts/oom-e8e927ba6c0d38274a37c1567560eb33baf74627.ttf", + "Tests/fonts/oom-4da0210eb7081b0bf15bf16cc4c52ce02c1e1bbc.ttf", ], ) def test_oom(test_file): diff --git a/src/_imagingft.c b/src/_imagingft.c index 02d54fe23..d421e5a0b 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if (width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From fd9bea271a521e1ce054b13c723e9e47a759187e Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 17 Jun 2023 14:39:34 +1000 Subject: [PATCH 383/512] Compare long long with long long MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/_imagingft.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_imagingft.c b/src/_imagingft.c index d421e5a0b..6cee021d4 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if ((long long)width * height > PyLong_AsLong(max_image_pixels) * 2) { + if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From f72dd8576ee77adec988dc1fe9777ee25f581a05 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:55:58 +0200 Subject: [PATCH 384/512] Changed `grabclipboard()` to use PNG compression on macOS Before, a lossy JPG compression was used. --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 39ecdf420..4de5c69fb 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -102,7 +102,7 @@ def grabclipboard(): + filepath + '" with write permission)', "try", - " write (the clipboard as JPEG picture) to theFile", + " write (the clipboard as «class PNGf») to theFile", "end try", "close access theFile", ] From 3c4ccdcff54a7890176e57b5daf6757891678110 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sat, 17 Jun 2023 12:59:42 +0200 Subject: [PATCH 385/512] Updated CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4af3fa516..5da3986ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 + [abey79] + - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere] From e52fa8fe386d5503bd3abb64773be208edcc58ca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 17 Jun 2023 21:01:52 +1000 Subject: [PATCH 386/512] Use relevant extension for temporary file --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 4de5c69fb..927033c60 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -95,7 +95,7 @@ def grab(bbox=None, include_layered_windows=False, all_screens=False, xdisplay=N def grabclipboard(): if sys.platform == "darwin": - fh, filepath = tempfile.mkstemp(".jpg") + fh, filepath = tempfile.mkstemp(".png") os.close(fh) commands = [ 'set theFile to (open for access POSIX file "' From 0440df0d83a7bfd92df130893432830e7d9b4183 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Jun 2023 20:14:23 +1000 Subject: [PATCH 387/512] Clarify that the changelog should not be updated in PRs [ci skip] --- .github/CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ba2b7d8ed..d03fcf0d9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -19,6 +19,7 @@ Please send a pull request to the `main` branch. Please include [documentation]( - Follow PEP 8. - When committing only documentation changes please include `[ci skip]` in the commit message to avoid running tests on AppVeyor. - Include [release notes](https://github.com/python-pillow/Pillow/tree/main/docs/releasenotes) as needed or appropriate with your bug fixes, feature additions and tests. +- Do not add to the [changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) for proposed changes, as that is updated after changes are merged. ## Reporting Issues From f28ecc5808ba1c562ebf7c557d7817f3fe92823d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jun 2023 17:45:24 +1000 Subject: [PATCH 388/512] Document how to install on MinGW when setuptools >= 60 --- docs/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/installation.rst b/docs/installation.rst index ac54b037d..dc1cd8653 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -312,6 +312,11 @@ Many of Pillow's features require external libraries: mingw-w64-x86_64-libimagequant \ mingw-w64-x86_64-libraqm + https://www.msys2.org/docs/python/ states that setuptools >= 60 does not work with + MSYS2. To workaround this, before installing Pillow you must run:: + + export SETUPTOOLS_USE_DISTUTILS=stdlib + .. tab:: FreeBSD .. Note:: Only FreeBSD 10 and 11 tested From cb8956fffb1bc0df9c74b2b789c716c64dd065b6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Jun 2023 23:27:45 +1000 Subject: [PATCH 389/512] Convert to HSV if mode is HSV in getcolor() --- Tests/test_imagecolor.py | 4 ++++ src/PIL/Image.py | 2 +- src/PIL/ImageColor.py | 16 ++++++++++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Tests/test_imagecolor.py b/Tests/test_imagecolor.py index dcc44e6e3..2fae6151c 100644 --- a/Tests/test_imagecolor.py +++ b/Tests/test_imagecolor.py @@ -193,6 +193,10 @@ def test_rounding_errors(): Image.new("LA", (1, 1), "white") +def test_color_hsv(): + assert (170, 255, 255) == ImageColor.getcolor("hsv(240, 100%, 100%)", "HSV") + + def test_color_too_long(): # Arrange color_too_long = "hsl(" + "1" * 40 + "," + "1" * 40 + "%," + "1" * 40 + "%)" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a785292f8..7b31178e3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2882,7 +2882,7 @@ def new(mode, size, color=0): :param color: What color to use for the image. Default is black. If given, this should be a single integer or floating point value for single-band modes, and a tuple for multi-band modes (one value - per band). When creating RGB images, you can also use color + per band). When creating RGB or HSV images, you can also use color strings as supported by the ImageColor module. If the color is None, the image is not initialised. :returns: An :py:class:`~PIL.Image.Image` object. diff --git a/src/PIL/ImageColor.py b/src/PIL/ImageColor.py index e184ed68d..befc1fd1d 100644 --- a/src/PIL/ImageColor.py +++ b/src/PIL/ImageColor.py @@ -122,9 +122,11 @@ def getrgb(color): def getcolor(color, mode): """ - Same as :py:func:`~PIL.ImageColor.getrgb`, but converts the RGB value to a - greyscale value if ``mode`` is not color or a palette image. If the string - cannot be parsed, this function raises a :py:exc:`ValueError` exception. + Same as :py:func:`~PIL.ImageColor.getrgb` for most modes. However, if + ``mode`` is HSV, converts the RGB value to a HSV value, or if ``mode`` is + not color or a palette image, converts the RGB value to a greyscale value. + If the string cannot be parsed, this function raises a :py:exc:`ValueError` + exception. .. versionadded:: 1.1.4 @@ -137,7 +139,13 @@ def getcolor(color, mode): if len(color) == 4: color, alpha = color[:3], color[3] - if Image.getmodebase(mode) == "L": + if mode == "HSV": + from colorsys import rgb_to_hsv + + r, g, b = color + h, s, v = rgb_to_hsv(r / 255, g / 255, b / 255) + return int(h * 255), int(s * 255), int(v * 255) + elif Image.getmodebase(mode) == "L": r, g, b = color # ITU-R Recommendation 601-2 for nonlinear RGB # scaled to 24 bits to match the convert's implementation. From 56a795c8ddaa203ebb3bed6df99c96d60b366199 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:05:03 -0500 Subject: [PATCH 390/512] add units to bench_cffi_access.py output --- Tests/bench_cffi_access.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 87cad699d..49ff34949 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -28,7 +28,7 @@ def timer(func, label, *args): func(*args) if time.time() - starttime > 10: print( - "{}: breaking at {} iterations, {:.6f} per iteration".format( + "{}: breaking at {} iterations, {:.6f}s per iteration".format( label, x + 1, (time.time() - starttime) / (x + 1.0) ) ) @@ -36,7 +36,7 @@ def timer(func, label, *args): if x == iterations - 1: endtime = time.time() print( - "{}: {:.4f} s {:.6f} per iteration".format( + "{}: {:.4f}s total, {:.6f}s per iteration".format( label, endtime - starttime, (endtime - starttime) / (x + 1.0) ) ) From ff4c7ffceaa1a67d036375d44020702347c65cbf Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 22 Jun 2023 09:16:18 -0500 Subject: [PATCH 391/512] use same print format regardless of iterations --- Tests/bench_cffi_access.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Tests/bench_cffi_access.py b/Tests/bench_cffi_access.py index 49ff34949..69ebef9b4 100644 --- a/Tests/bench_cffi_access.py +++ b/Tests/bench_cffi_access.py @@ -27,25 +27,19 @@ def timer(func, label, *args): for x in range(iterations): func(*args) if time.time() - starttime > 10: - print( - "{}: breaking at {} iterations, {:.6f}s per iteration".format( - label, x + 1, (time.time() - starttime) / (x + 1.0) - ) - ) break - if x == iterations - 1: - endtime = time.time() - print( - "{}: {:.4f}s total, {:.6f}s per iteration".format( - label, endtime - starttime, (endtime - starttime) / (x + 1.0) - ) + endtime = time.time() + print( + "{}: completed {} iterations in {:.4f}s, {:.6f}s per iteration".format( + label, x + 1, endtime - starttime, (endtime - starttime) / (x + 1.0) ) + ) def test_direct(): im = hopper() im.load() - # im = Image.new( "RGB", (2000, 2000), (1, 3, 2)) + # im = Image.new("RGB", (2000, 2000), (1, 3, 2)) caccess = im.im.pixel_access(False) access = PyAccess.new(im, False) From d6f19625e8cadc92e792f3567c9e82ee057653fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Jun 2023 21:52:00 +1000 Subject: [PATCH 392/512] Removed support for 32-bit --- .appveyor.yml | 4 +- .github/workflows/test-docker.yml | 4 +- .github/workflows/test-mingw.yml | 63 +++++++++--------------------- .github/workflows/test-windows.yml | 18 +++------ Tests/32bit_segfault_check.py | 8 ---- Tests/check_large_memory.py | 5 --- Tests/check_large_memory_numpy.py | 5 --- Tests/test_core_resources.py | 5 --- Tests/test_file_webp.py | 2 - Tests/test_image_putdata.py | 5 +-- Tests/test_map.py | 3 -- docs/installation.rst | 17 +++----- setup.py | 20 ++++------ src/libImaging/ImagingUtils.h | 2 +- winbuild/build.rst | 6 +-- winbuild/build_prepare.py | 8 +--- 16 files changed, 47 insertions(+), 128 deletions(-) delete mode 100755 Tests/32bit_segfault_check.py diff --git a/.appveyor.yml b/.appveyor.yml index 36f5bd0ad..9a2eef767 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,8 +10,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python311 - ARCHITECTURE: x86 + - PYTHON: C:/Python311-x64 + ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 3bcb8cfbc..f22733dc4 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -38,8 +38,8 @@ jobs: centos-7-amd64, centos-stream-8-amd64, centos-stream-9-amd64, - debian-11-bullseye-x86, - debian-12-bookworm-x86, + debian-11-bullseye-amd64, + debian-12-bookworm-amd64, fedora-37-amd64, fedora-38-amd64, gentoo, diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index a109ec0d8..4269eeb62 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -21,27 +21,16 @@ concurrency: jobs: build: runs-on: windows-latest - strategy: - fail-fast: false - matrix: - mingw: ["MINGW32", "MINGW64"] - include: - - mingw: "MINGW32" - name: "MSYS2 MinGW 32-bit" - package: "mingw-w64-i686" - - mingw: "MINGW64" - name: "MSYS2 MinGW 64-bit" - package: "mingw-w64-x86_64" defaults: run: shell: bash.exe --login -eo pipefail "{0}" env: - MSYSTEM: ${{ matrix.mingw }} + MSYSTEM: MINGW64 CHERE_INVOKING: 1 timeout-minutes: 30 - name: ${{ matrix.name }} + name: "MSYS2 MinGW" steps: - name: Checkout Pillow @@ -54,26 +43,22 @@ jobs: - name: Install dependencies run: | pacman -S --noconfirm \ - ${{ matrix.package }}-freetype \ - ${{ matrix.package }}-gcc \ - ${{ matrix.package }}-ghostscript \ - ${{ matrix.package }}-lcms2 \ - ${{ matrix.package }}-libimagequant \ - ${{ matrix.package }}-libjpeg-turbo \ - ${{ matrix.package }}-libraqm \ - ${{ 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 - - if [ ${{ matrix.package }} == "mingw-w64-x86_64" ]; then - pacman -S --noconfirm \ - ${{ matrix.package }}-python-pyqt6 - fi + mingw-w64-x86_64-freetype \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-ghostscript \ + mingw-w64-x86_64-lcms2 \ + mingw-w64-x86_64-libimagequant \ + mingw-w64-x86_64-libjpeg-turbo \ + mingw-w64-x86_64-libraqm \ + mingw-w64-x86_64-libtiff \ + mingw-w64-x86_64-libwebp \ + mingw-w64-x86_64-openjpeg2 \ + mingw-w64-x86_64-python3-cffi \ + mingw-w64-x86_64-python3-numpy \ + mingw-w64-x86_64-python3-olefile \ + mingw-w64-x86_64-python3-pip \ + mingw-w64-x86_64-python3-setuptools \ + mingw-w64-x86_64-python-pyqt6 python3 -m pip install pyroma pytest pytest-cov pytest-timeout @@ -93,14 +78,4 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ matrix.name }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: MinGW Test Successful - steps: - - name: Success - run: echo MinGW Test Successful + name: "MSYS2 MinGW" diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index cab47b01f..b5fd4395f 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,18 +24,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["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 - - python-version: "pypy3.9" - architecture: "x64" - - python-version: "pypy3.10" - architecture: "x64" + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy3.9", "pypy3.10"] timeout-minutes: 30 - name: Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: Python ${{ matrix.python-version }} steps: - name: Checkout Pillow @@ -58,7 +51,6 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - architecture: ${{ matrix.architecture }} cache: pip cache-dependency-path: ".github/workflows/test-windows.yml" @@ -206,14 +198,14 @@ jobs: with: file: ./coverage.xml flags: GHA_Windows - name: ${{ runner.os }} Python ${{ matrix.python-version }} ${{ matrix.architecture }} + name: ${{ runner.os }} Python ${{ matrix.python-version }} - name: Build wheel id: wheel if: "github.event_name != 'pull_request'" run: | - mkdir fribidi\${{ matrix.architecture }} - copy winbuild\build\bin\fribidi* fribidi\${{ matrix.architecture }} + mkdir fribidi + copy winbuild\build\bin\fribidi* fribidi setlocal EnableDelayedExpansion for %%f in (winbuild\build\license\*) do ( set x=%%~nf diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py deleted file mode 100755 index 2ff7f908f..000000000 --- a/Tests/32bit_segfault_check.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -from PIL import Image - -if sys.maxsize < 2**32: - im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index d98f4a694..219788d7b 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -23,9 +21,6 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - def _write_png(tmp_path, xdim, ydim): f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index 24cb1f722..c54894721 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -19,9 +17,6 @@ YDIM = 32769 XDIM = 48000 -pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") - - def _write_png(tmp_path, xdim, ydim): dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index 9021a9fb3..f2105d6ca 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -110,9 +108,6 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_blocks_max(-1) - if sys.maxsize < 2**32: - with pytest.raises(ValueError): - Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a7b6c735a..01b11447a 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,6 +1,5 @@ import io import re -import sys import warnings import pytest @@ -145,7 +144,6 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) - @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 0e6293349..db5307d2c 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,10 +38,7 @@ def test_long_integers(): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - if sys.maxsize > 2**32: - assert put(sys.maxsize) == (255, 255, 255, 255) - else: - assert put(sys.maxsize) == (255, 255, 255, 127) + assert put(sys.maxsize) == (255, 255, 255, 255) def test_pypy_performance(): diff --git a/Tests/test_map.py b/Tests/test_map.py index d816bddaf..42b6f7cdd 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,5 +1,3 @@ -import sys - import pytest from PIL import Image @@ -36,7 +34,6 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels -@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/docs/installation.rst b/docs/installation.rst index dc1cd8653..79e14d478 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -285,11 +285,8 @@ Many of Pillow's features require external libraries: .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or - **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. - - The following instructions target the 64-bit build, for 32-bit - replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 64-bit** console, + *not* **MSYS2** directly. Make sure you have Python and GCC installed:: @@ -339,8 +336,6 @@ Many of Pillow's features require external libraries: pkg install -y python ndk-sysroot clang make \ libjpeg-turbo - This has been tested within the Termux app on ChromeOS, on x86. - Installing ^^^^^^^^^^ @@ -451,9 +446,9 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | CentOS Stream 9 | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 11 Bullseye | 3.9 | x86 | +| Debian 11 Bullseye | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86 | +| Debian 12 Bookworm | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -474,10 +469,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Windows Server 2016 | 3.8 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86, x86-64 | +| Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ -| | 3.9 (MinGW) | x86, x86-64 | +| | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.8, 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ diff --git a/setup.py b/setup.py index 7c1ad6dc5..b647944d3 100755 --- a/setup.py +++ b/setup.py @@ -153,16 +153,13 @@ def _find_library_dirs_ldconfig(): ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - if struct.calcsize("l") == 4: - machine = os.uname()[4] + "-32" - else: - machine = os.uname()[4] + "-64" + machine = os.uname()[4] mach_map = { - "x86_64-64": "libc6,x86-64", - "ppc64-64": "libc6,64bit", - "sparc64-64": "libc6,64bit", - "s390x-64": "libc6,64bit", - "ia64-64": "libc6,IA-64", + "x86_64": "libc6,x86-64", + "ppc64": "libc6,64bit", + "sparc64": "libc6,64bit", + "s390x": "libc6,64bit", + "ia64": "libc6,IA-64", } abi_type = mach_map.get(machine, "libc6") @@ -584,10 +581,7 @@ class pil_build_ext(build_ext): # user libs are at $PREFIX/lib _add_directory( library_dirs, - os.path.join( - os.environ["ANDROID_ROOT"], - "lib" if struct.calcsize("l") == 4 else "lib64", - ), + os.path.join(os.environ["ANDROID_ROOT"], "lib64"), ) elif sys.platform.startswith("netbsd"): diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index 0c0c1eda9..f2acabeac 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -30,7 +30,7 @@ /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ -#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ +#if defined(__SSE__) && !defined(__NO_INLINE__) && \ !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; diff --git a/winbuild/build.rst b/winbuild/build.rst index 99dfad301..97df950b3 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x86/x64: `Netwide Assembler (NASM) `_ +* x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] + [--architecture {x64,ARM64}] [--python PYTHON] [--executable EXECUTABLE] [--nmake] [--no-imagequant] [--no-fribidi] @@ -56,7 +56,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x86,x64,ARM64} + --architecture {x64,ARM64} build architecture (default: same as host Python) --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 89b2daad0..d8212ee51 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -3,7 +3,6 @@ import os import platform import re import shutil -import struct import subprocess import sys @@ -98,7 +97,6 @@ def cmd_msbuild( SF_PROJECTS = "https://sourceforge.net/projects" architectures = { - "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -611,11 +609,7 @@ if __name__ == "__main__": choices=architectures, default=os.environ.get( "ARCHITECTURE", - ( - "ARM64" - if platform.machine() == "ARM64" - else ("x86" if struct.calcsize("P") == 4 else "x64") - ), + "ARM64" if platform.machine() == "ARM64" else "x64", ), help="build architecture (default: same as host Python)", ) From 43a12542ad83c8da7aa138fbf376b22895775e37 Mon Sep 17 00:00:00 2001 From: Rozie <60040522+RoziePlaysPython@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:05:28 +0300 Subject: [PATCH 393/512] Update Image.show docs to list all viewers used on linux [ci skip] Accurate description of how default viewer is chosen --- 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 a785292f8..c19bdcac8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,7 +2451,7 @@ class Image: The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **display**, **eog** or + On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From b0b079882047ca2853714ba899ef719cc2ed70f9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Fri, 23 Jun 2023 22:22:33 +1000 Subject: [PATCH 394/512] Lint fix --- 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 c19bdcac8..97f3f4926 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2451,8 +2451,8 @@ class Image: The image is first saved to a temporary file. By default, it will be in PNG format. - On Unix, the image is then opened using the **xdg-open**, **display**, **gm**, **eog** or - **xv** utility, depending on which one can be found. + On Unix, the image is then opened using the **xdg-open**, **display**, + **gm**, **eog** or **xv** utility, depending on which one can be found. On macOS, the image is opened with the native Preview application. From 6c464b810185494df6cd1c64ed21ae455ebc93c8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:09:07 +1000 Subject: [PATCH 395/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5da3986ba..c05268772 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,11 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Fixed finding dependencies on Cygwin #7175 + [radarhere] + - Changed grabclipboard() to use PNG instead of JPG compression on macOS #7219 - [abey79] + [abey79, radarhere] - Added in_place argument to ImageOps.exif_transpose() #7092 [radarhere] From 5498cb800c8a432f709cffde219d38ef521fd4e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:46:42 +1000 Subject: [PATCH 396/512] Order slower jobs first Co-authored-by: Hugo van Kemenade --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index b5fd4395f..3a24fd36a 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12-dev", "pypy3.9", "pypy3.10"] + python-version: ["pypy3.10", "pypy3.9", "3.8", "3.9", "3.10", "3.11", "3.12-dev"] timeout-minutes: 30 From 9c5175d04871647dfae3a3286b9b60d250a0566f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 17:52:28 +1000 Subject: [PATCH 397/512] Added release notes --- docs/releasenotes/10.0.0.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 3ffafafdc..ec3a6927e 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -4,6 +4,11 @@ Backwards Incompatible Changes ============================== +32-bit support +^^^^^^^^^^^^^^ + +32-bit architecture is no longer supported. + Categories ^^^^^^^^^^ From b6751b24de19770da2b18ebd112ef069c7e741dc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 19:17:30 +1000 Subject: [PATCH 398/512] Updated mergify --- .github/mergify.yml | 2 +- .github/workflows/test-mingw.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/mergify.yml b/.github/mergify.yml index 8dfa07f4e..3c2066137 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -7,7 +7,7 @@ pull_request_rules: - status-success=Test Successful - status-success=Docker Test Successful - status-success=Windows Test Successful - - status-success=MinGW Test Successful + - status-success=MinGW - status-success=Cygwin Test Successful - status-success=continuous-integration/appveyor/pr actions: diff --git a/.github/workflows/test-mingw.yml b/.github/workflows/test-mingw.yml index 4269eeb62..59fd5ed9d 100644 --- a/.github/workflows/test-mingw.yml +++ b/.github/workflows/test-mingw.yml @@ -30,7 +30,7 @@ jobs: CHERE_INVOKING: 1 timeout-minutes: 30 - name: "MSYS2 MinGW" + name: "MinGW" steps: - name: Checkout Pillow From abf05414de5eecf5b3115a86e72247adc6ceaebd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 21:52:13 +1000 Subject: [PATCH 399/512] Mention that 32-bit wheels are no longer provided --- docs/releasenotes/10.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index ec3a6927e..d33b75e4d 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -7,7 +7,7 @@ Backwards Incompatible Changes 32-bit support ^^^^^^^^^^^^^^ -32-bit architecture is no longer supported. +32-bit architecture is no longer supported and 32-bit wheels are no longer provided. Categories ^^^^^^^^^^ From 83867f5c35fe239d5fefd15081d74ac480be5a52 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 22:08:52 +1000 Subject: [PATCH 400/512] Updated freetype to 2.13.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 89b2daad0..b9de071a0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -237,9 +237,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "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", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.1.tar.gz", # noqa: E501 + "filename": "freetype-2.13.1.tar.gz", + "dir": "freetype-2.13.1", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 5d0e37e255aca3aabb97a619ca46e2f9bdb4f3a8 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:13:26 +0100 Subject: [PATCH 401/512] use --config-settings when building on Windows --- .appveyor.yml | 16 ++--- .github/workflows/test-windows.yml | 13 ++-- winbuild/build_prepare.py | 102 +++++++++-------------------- 3 files changed, 44 insertions(+), 87 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 575b6caa6..60132a9a3 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -38,10 +38,9 @@ install: - path C:\pillow\winbuild\build\bin;%PATH% build_script: -- ps: | - c:\pillow\winbuild\build\build_pillow.cmd install - $host.SetShouldExit(0) - cd c:\pillow +- winbuild\build\build_env.cmd +- '%PYTHON%\%EXECUTABLE% -m pip install -v -C raqm=vendor -C fribidi=vendor .' - '%PYTHON%\%EXECUTABLE% selftest.py --installed' test_script: @@ -63,18 +62,15 @@ cache: - '%LOCALAPPDATA%\pip\Cache' artifacts: -- path: pillow\dist\*.egg +- path: pillow\*.egg name: egg -- path: pillow\dist\*.wheel +- path: pillow\*.whl name: wheel before_deploy: - cd c:\pillow - - '%PYTHON%\%EXECUTABLE% -m pip install wheel' - - cd c:\pillow\winbuild\ - - c:\pillow\winbuild\build\build_pillow.cmd --global-option="bdist_wheel" - - cd c:\pillow - - ps: Get-ChildItem .\dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } + - '%PYTHON%\%EXECUTABLE% -m pip wheel -v -C raqm=vendor -C fribidi=vendor .' + - ps: Get-ChildItem .\*.whl | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } deploy: provider: S3 diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 3bfd54ee3..821e037a2 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -97,7 +97,7 @@ jobs: - name: Prepare build if: steps.build-cache.outputs.cache-hit != 'true' run: | - & python.exe winbuild\build_prepare.py -v --python $env:pythonLocation + & python.exe winbuild\build_prepare.py -v shell: pwsh - name: Build dependencies / libjpeg-turbo @@ -165,9 +165,9 @@ jobs: - name: Build Pillow run: | - $FLAGS="" - if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS='--global-option="--disable-imagequant"' } - & winbuild\build\build_pillow.cmd $FLAGS --global-option="install" + $FLAGS="-C raqm=vendor -C fribidi=vendor" + if ('${{ github.event_name }}' -ne 'pull_request') { $FLAGS+=" -C imagequant=disable" } + cmd /c "winbuild\build\build_env.cmd && $env:pythonLocation\python.exe -m pip install -v $FLAGS ." & $env:pythonLocation\python.exe selftest.py --installed shell: pwsh @@ -231,7 +231,8 @@ jobs: ) ) for /f "tokens=3 delims=/" %%a in ("${{ github.ref }}") do echo dist=dist-%%a >> %GITHUB_OUTPUT% - winbuild\\build\\build_pillow.cmd --global-option="--disable-imagequant" --global-option="bdist_wheel" + call winbuild\\build\\build_env.cmd + %pythonLocation%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor -C imagequant=disable . shell: cmd - name: Upload wheel @@ -239,7 +240,7 @@ jobs: if: "github.event_name != 'pull_request'" with: name: ${{ steps.wheel.outputs.dist }} - path: dist\*.whl + path: "*.whl" - name: Upload fribidi.dll if: "github.event_name != 'pull_request' && matrix.python-version == 3.11" diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b41a735db..e7ccf3385 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -103,13 +103,6 @@ architectures = { "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } -header = [ - cmd_set("INCLUDE", "{inc_dir}"), - cmd_set("INCLIB", "{lib_dir}"), - cmd_set("LIB", "{lib_dir}"), - cmd_append("PATH", "{bin_dir}"), -] - # dependencies, listed in order of compilation deps = { "libjpeg": { @@ -401,23 +394,12 @@ def find_msvs(): print("Visual Studio seems to be missing C compiler") return None - vs = { - "header": [], - # nmake selected by vcvarsall - "nmake": "nmake.exe", - "vs_dir": vspath, - } - # vs2017 msbuild = os.path.join(vspath, "MSBuild", "15.0", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): # vs2019 msbuild = os.path.join(vspath, "MSBuild", "Current", "Bin", "MSBuild.exe") - if os.path.isfile(msbuild): - vs["msbuild"] = f'"{msbuild}"' - else: + if not os.path.isfile(msbuild): print("Visual Studio MSBuild not found") return None @@ -425,9 +407,13 @@ def find_msvs(): if not os.path.isfile(vcvarsall): print("Visual Studio vcvarsall not found") return None - vs["header"].append(f'call "{vcvarsall}" {{vcvars_arch}}') - return vs + return { + "vs_dir": vspath, + "msbuild": f'"{msbuild}"', + "vcvarsall": f'"{vcvarsall}"', + "nmake": "nmake.exe", # nmake selected by vcvarsall + } def extract_dep(url, filename): @@ -497,6 +483,22 @@ def get_footer(dep): return lines +def build_env(): + lines = [ + "if defined DISTUTILS_USE_SDK goto end", + cmd_set("INCLUDE", "{inc_dir}"), + cmd_set("INCLIB", "{lib_dir}"), + cmd_set("LIB", "{lib_dir}"), + cmd_append("PATH", "{bin_dir}"), + f"call {{vcvarsall}} {{vcvars_arch}}", + cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow + cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT + ":end", + "@echo on", + ] + write_script("build_env.cmd", lines) + + def build_dep(name): dep = deps[name] dir = dep["dir"] @@ -534,11 +536,11 @@ def build_dep(name): banner = f"Building {name} ({dir})" lines = [ + rf'call "{{build_dir}}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), - "cd /D %s" % os.path.join(sources_dir, dir), - *prefs["header"], + cmd_cd(os.path.join(sources_dir, dir)), *dep.get("build", []), *get_footer(dep), ] @@ -548,7 +550,7 @@ def build_dep(name): def build_dep_all(): - lines = ["@echo on"] + lines = [r'call "{build_dir}\build_env.cmd"'] for dep_name in deps: print() if dep_name in disabled: @@ -562,32 +564,16 @@ def build_dep_all(): write_script("build_dep_all.cmd", lines) -def build_pillow(): - lines = [ - "@echo ---- Building Pillow (build_ext %*) ----", - cmd_cd("{pillow_dir}"), - *prefs["header"], - cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow - cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT - r'"{python_dir}\{python_exe}" -m pip install . ' - r'--global-option="--vendor-raqm" ' - r'--global-option="--vendor-fribidi" ' - r"%*", - ] - - write_script("build_pillow.cmd", lines) - - if __name__ == "__main__": winbuild_dir = os.path.dirname(os.path.realpath(__file__)) pillow_dir = os.path.realpath(os.path.join(winbuild_dir, "..")) parser = argparse.ArgumentParser( prog="winbuild\\build_prepare.py", - description="Download dependencies and generate build scripts for Pillow.", + description="Download and generate build scripts for Pillow dependencies.", epilog="""Arguments can also be supplied using the environment variables - PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. - See winbuild\\build.rst for more information.""", + PILLOW_BUILD, PILLOW_DEPS, ARCHITECTURE. See winbuild\\build.rst + for more information.""", ) parser.add_argument( "-v", "--verbose", action="store_true", help="print generated scripts" @@ -622,20 +608,6 @@ if __name__ == "__main__": ), help="build architecture (default: same as host Python)", ) - parser.add_argument( - "--python", - dest="python_dir", - metavar="PYTHON", - default=os.environ.get("PYTHON"), - help="Python install directory (default: use host Python)", - ) - parser.add_argument( - "--executable", - dest="python_exe", - metavar="EXECUTABLE", - default=os.environ.get("EXECUTABLE", "python.exe"), - help="Python executable (default: use host Python)", - ) parser.add_argument( "--nmake", dest="cmake_generator", @@ -660,11 +632,6 @@ if __name__ == "__main__": arch_prefs = architectures[args.architecture] print("Target architecture:", args.architecture) - if args.python_dir is None: - args.python_dir = os.path.dirname(os.path.realpath(sys.executable)) - args.python_exe = os.path.basename(sys.executable) - print("Target Python:", os.path.join(args.python_dir, args.python_exe)) - msvs = find_msvs() if msvs is None: msg = "Visual Studio not found. Please install Visual Studio 2017 or newer." @@ -702,9 +669,6 @@ if __name__ == "__main__": disabled += ["fribidi"] prefs = { - # Python paths / preferences - "python_dir": args.python_dir, - "python_exe": args.python_exe, "architecture": args.architecture, **arch_prefs, # Pillow paths @@ -722,8 +686,6 @@ if __name__ == "__main__": "cmake": "cmake.exe", # TODO find CMAKE automatically "cmake_generator": args.cmake_generator, # TODO find NASM automatically - # script header - "header": sum([header, msvs["header"], ["@echo on"]], []), } for k, v in deps.items(): @@ -732,7 +694,5 @@ if __name__ == "__main__": print() write_script(".gitignore", ["*"]) + build_env() build_dep_all() - if args.verbose: - print() - build_pillow() From 466aa7e6c4cdbeab794c1e024b0e95e80a6bb1a2 Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:52:59 +0100 Subject: [PATCH 402/512] update winbuild documentation --- winbuild/README.md | 6 +++--- winbuild/build.rst | 36 +++++++++++++++++------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/winbuild/README.md b/winbuild/README.md index 2975acf28..7e81abcb0 100644 --- a/winbuild/README.md +++ b/winbuild/README.md @@ -18,12 +18,12 @@ The following is a simplified version of the script used on AppVeyor: ``` set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild -C:\Python39\bin\python.exe build_prepare.py -v --depends=C:\pillow-depends +%PYTHON%\python.exe build_prepare.py -v --depends=C:\pillow-depends build\build_dep_all.cmd -build\build_pillow.cmd install cd .. +%PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests -build\build_pillow.cmd bdist_wheel +%PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . ``` diff --git a/winbuild/build.rst b/winbuild/build.rst index 99dfad301..a8e4ebaa6 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -42,11 +42,10 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x86,x64,ARM64}] - [--python PYTHON] [--executable EXECUTABLE] - [--nmake] [--no-imagequant] [--no-fribidi] + [--architecture {x86,x64,ARM64}] [--nmake] + [--no-imagequant] [--no-fribidi] - Download dependencies and generate build scripts for Pillow. + Download and generate build scripts for Pillow dependencies. options: -h, --help show this help message and exit @@ -58,17 +57,13 @@ Run ``build_prepare.py`` to configure the build:: 'winbuild\depends') --architecture {x86,x64,ARM64} build architecture (default: same as host Python) - --python PYTHON Python install directory (default: use host Python) - --executable EXECUTABLE - Python executable (default: use host Python) --nmake build dependencies using NMake instead of Ninja --no-imagequant skip GPL-licensed optional dependency libimagequant --no-fribidi, --no-raqm skip LGPL-licensed optional dependency FriBiDi Arguments can also be supplied using the environment variables PILLOW_BUILD, - PILLOW_DEPS, ARCHITECTURE, PYTHON, EXECUTABLE. See winbuild\build.rst for more - information. + PILLOW_DEPS, ARCHITECTURE. See winbuild\build.rst for more information. **Warning:** The build directory is wiped when ``build_prepare.py`` is run. @@ -86,14 +81,16 @@ or run the individual scripts in order to build each dependency separately. Building Pillow --------------- -Once the dependencies are built, run -``winbuild\build\build_pillow.cmd install`` to build and install -Pillow for the selected version of Python. -``winbuild\build\build_pillow.cmd bdist_wheel`` will build wheels -instead of installing Pillow. +Once the dependencies are built, make sure the required environment variables +are set by running ``winbuild\build\build_env.cmd`` and install Pillow with pip:: -You can also use ``winbuild\build\build_pillow.cmd --inplace develop`` to build -and install Pillow in develop mode (instead of ``python3 -m pip install --editable``). + winbuild\build\build_env.cmd + python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . + +To build a wheel instead, run:: + + winbuild\build\build_env.cmd + python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . Testing Pillow -------------- @@ -112,11 +109,12 @@ The following is a simplified version of the script used on AppVeyor:: set PYTHON=C:\Python38\bin cd /D C:\Pillow\winbuild - C:\Python39\bin\python.exe build_prepare.py -v --depends C:\pillow-depends + %PYTHON%\python.exe build_prepare.py -v --depends C:\pillow-depends build\build_dep_all.cmd - build\build_pillow.cmd install + build\build_env.cmd cd .. + %PYTHON%\python.exe -m pip install -v -C raqm=vendor -C fribidi=vendor . path C:\Pillow\winbuild\build\bin;%PATH% %PYTHON%\python.exe selftest.py %PYTHON%\python.exe -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests - build\build_pillow.cmd bdist_wheel + %PYTHON%\python.exe -m pip wheel -v -C raqm=vendor -C fribidi=vendor . From 14ae3e446cdd24c6304513b109e650cdc03a2d57 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:49:47 +1000 Subject: [PATCH 403/512] Updated pyproject-fmt to 0.12.1 (cherry picked from commit 0e1f86a425075db3a755db2e81502ec6eb6ba9e0) --- .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 79966503f..872c73843 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.11.2 + rev: 0.12.1 hooks: - id: pyproject-fmt From 25c24a8a9157d252e9edb468c70eea04e5b9e246 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Jun 2023 13:50:27 +1000 Subject: [PATCH 404/512] Removed unused code (cherry picked from commit 600b823de6acbe6c3920633af4c64f6db61565d4) --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e2ed3b315..024634ad8 100755 --- a/setup.py +++ b/setup.py @@ -137,7 +137,6 @@ class RequiredDependencyException(Exception): PLATFORM_MINGW = os.name == "nt" and "GCC" in sys.version -PLATFORM_PYPY = hasattr(sys, "pypy_version_info") def _dbg(s, tp=None): From c1799627df23bcda120cb9c278fb3c292e2b235e Mon Sep 17 00:00:00 2001 From: nulano Date: Sat, 24 Jun 2023 15:57:43 +0100 Subject: [PATCH 405/512] lint fixes --- pyproject.toml | 9 +++++++-- winbuild/build_prepare.py | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1d32e7d90..93a433608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,9 @@ [build-system] -requires = ["setuptools >= 67.8", "wheel"] build-backend = "backend" -backend-path = ["_custom_build"] +requires = [ + "setuptools>=67.8", + "wheel", +] +backend-path = [ + "_custom_build", +] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index e7ccf3385..fcc433942 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -5,7 +5,6 @@ import re import shutil import struct import subprocess -import sys def cmd_cd(path): @@ -490,7 +489,7 @@ def build_env(): cmd_set("INCLIB", "{lib_dir}"), cmd_set("LIB", "{lib_dir}"), cmd_append("PATH", "{bin_dir}"), - f"call {{vcvarsall}} {{vcvars_arch}}", + "call {vcvarsall} {vcvars_arch}", cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("py_vcruntime_redist", "true"), # always use /MD, never /MT ":end", @@ -536,7 +535,7 @@ def build_dep(name): banner = f"Building {name} ({dir})" lines = [ - rf'call "{{build_dir}}\build_env.cmd"', + r'call "{build_dir}\build_env.cmd"', "@echo " + ("=" * 70), f"@echo ==== {banner:<60} ====", "@echo " + ("=" * 70), From 9c73834407677b774b104a25428a8320f44649ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jun 2023 13:33:20 +1000 Subject: [PATCH 406/512] Removed deleted file --- codecov.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index b794632fa..fe8c126ed 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,7 +16,6 @@ coverage: # Matches 'omit:' in .coveragerc ignore: - - "Tests/32bit_segfault_check.py" - "Tests/bench_cffi_access.py" - "Tests/check_*.py" - "Tests/createfontdatachunk.py" From 837d23f27386da5dd4b5b3812c041399e1c600e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 25 Jun 2023 13:58:02 +1000 Subject: [PATCH 407/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c05268772..9d95f3cd2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,15 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Removed support for 32-bit #7228 + [radarhere, hugovk] + +- Use --config-settings instead of deprecated --global-option #7171 + [radarhere] + +- Better C integer definitions #6645 + [Yay295, hugovk] + - Fixed finding dependencies on Cygwin #7175 [radarhere] From d12783374b3bdd40a156cc0e63b10aa17b5c1ab6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 25 Jun 2023 21:39:26 +1000 Subject: [PATCH 408/512] Increased coverage threshold --- codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codecov.yml b/codecov.yml index fe8c126ed..40419979f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -12,7 +12,7 @@ coverage: status: project: default: - threshold: 0.01% + threshold: 0.1% # Matches 'omit:' in .coveragerc ignore: From fec793d8ab8bf2065ef0eb188966e4e2337c329e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ondrej=20Baranovi=C4=8D?= Date: Sun, 25 Jun 2023 13:05:22 +0100 Subject: [PATCH 409/512] don't explicitly install wheel Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- .github/workflows/test-windows.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 20de322f6..70afbab24 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -57,8 +57,8 @@ jobs: - name: Print build system information run: python3 .github/workflows/system-info.py - - name: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml - run: python3 -m pip install wheel pytest pytest-cov pytest-timeout defusedxml + - name: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml + run: python3 -m pip install pytest pytest-cov pytest-timeout defusedxml - name: Install dependencies id: install From 51cd79266dac9917c7a2442849041aae5368a820 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 13:51:17 +1000 Subject: [PATCH 410/512] Updated libtiff to 4.5.1 --- docs/installation.rst | 2 +- winbuild/build_prepare.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index e29709bff..6262548d8 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -155,7 +155,7 @@ Many of Pillow's features require external libraries: * **libtiff** provides compressed TIFF functionality - * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5** + * Pillow has been tested with libtiff versions **3.x** and **4.0-4.5.1** * **libfreetype** provides type related services diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9984eb1bc..a9b6450d8 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -186,9 +186,9 @@ deps = { "libs": [r"output\release-static\{architecture}\lib\*.lib"], }, "libtiff": { - "url": "https://download.osgeo.org/libtiff/tiff-4.5.0.tar.gz", - "filename": "tiff-4.5.0.tar.gz", - "dir": "tiff-4.5.0", + "url": "https://download.osgeo.org/libtiff/tiff-4.5.1.tar.gz", + "filename": "tiff-4.5.1.tar.gz", + "dir": "tiff-4.5.1", "license": "LICENSE.md", "patch": { r"libtiff\tif_lzma.c": { @@ -199,6 +199,12 @@ deps = { # link against webp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 }, + r"test/CMakeLists.txt": { + "add_executable(test_write_read_tags ../placeholder.h)": "", + "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 + "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", + "list(APPEND simple_tests test_write_read_tags)": "", + }, }, "build": [ *cmds_cmake( From 45c9dcf123948069a6e28e39413b197ebf3549a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 14:43:58 +1000 Subject: [PATCH 411/512] Restored 32-bit support --- Tests/32bit_segfault_check.py | 8 ++++++++ Tests/check_large_memory.py | 5 +++++ Tests/check_large_memory_numpy.py | 5 +++++ Tests/test_core_resources.py | 5 +++++ Tests/test_file_webp.py | 2 ++ Tests/test_image_putdata.py | 5 ++++- Tests/test_map.py | 3 +++ codecov.yml | 1 + docs/installation.rst | 9 +++++++-- docs/releasenotes/10.0.0.rst | 18 +++++------------- setup.py | 20 +++++++++++++------- src/libImaging/ImagingUtils.h | 2 +- winbuild/build.rst | 6 +++--- winbuild/build_prepare.py | 8 +++++++- 14 files changed, 69 insertions(+), 28 deletions(-) create mode 100755 Tests/32bit_segfault_check.py diff --git a/Tests/32bit_segfault_check.py b/Tests/32bit_segfault_check.py new file mode 100755 index 000000000..2ff7f908f --- /dev/null +++ b/Tests/32bit_segfault_check.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +import sys + +from PIL import Image + +if sys.maxsize < 2**32: + im = Image.new("L", (999999, 999999), 0) diff --git a/Tests/check_large_memory.py b/Tests/check_large_memory.py index 219788d7b..d98f4a694 100644 --- a/Tests/check_large_memory.py +++ b/Tests/check_large_memory.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -21,6 +23,9 @@ YDIM = 32769 XDIM = 48000 +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + def _write_png(tmp_path, xdim, ydim): f = str(tmp_path / "temp.png") im = Image.new("L", (xdim, ydim), 0) diff --git a/Tests/check_large_memory_numpy.py b/Tests/check_large_memory_numpy.py index c54894721..24cb1f722 100644 --- a/Tests/check_large_memory_numpy.py +++ b/Tests/check_large_memory_numpy.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -17,6 +19,9 @@ YDIM = 32769 XDIM = 48000 +pytestmark = pytest.mark.skipif(sys.maxsize <= 2**32, reason="requires 64-bit system") + + def _write_png(tmp_path, xdim, ydim): dtype = np.uint8 a = np.zeros((xdim, ydim), dtype=dtype) diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py index f2105d6ca..9021a9fb3 100644 --- a/Tests/test_core_resources.py +++ b/Tests/test_core_resources.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -108,6 +110,9 @@ class TestCoreMemory: with pytest.raises(ValueError): Image.core.set_blocks_max(-1) + if sys.maxsize < 2**32: + with pytest.raises(ValueError): + Image.core.set_blocks_max(2**29) @pytest.mark.skipif(is_pypy(), reason="Images not collected") def test_set_blocks_max_stats(self): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 01b11447a..a7b6c735a 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -1,5 +1,6 @@ import io import re +import sys import warnings import pytest @@ -144,6 +145,7 @@ class TestFileWebp: self._roundtrip(tmp_path, "P", 50.0) + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path): temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (15000, 15000)) diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index db5307d2c..0e6293349 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -38,7 +38,10 @@ def test_long_integers(): assert put(0xFFFFFFFF) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) assert put(-1) == (255, 255, 255, 255) - assert put(sys.maxsize) == (255, 255, 255, 255) + if sys.maxsize > 2**32: + assert put(sys.maxsize) == (255, 255, 255, 255) + else: + assert put(sys.maxsize) == (255, 255, 255, 127) def test_pypy_performance(): diff --git a/Tests/test_map.py b/Tests/test_map.py index 42b6f7cdd..d816bddaf 100644 --- a/Tests/test_map.py +++ b/Tests/test_map.py @@ -1,3 +1,5 @@ +import sys + import pytest from PIL import Image @@ -34,6 +36,7 @@ def test_tobytes(): Image.MAX_IMAGE_PIXELS = max_pixels +@pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_ysize(): numpy = pytest.importorskip("numpy", reason="NumPy not installed") diff --git a/codecov.yml b/codecov.yml index 40419979f..1ea7974eb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -16,6 +16,7 @@ coverage: # Matches 'omit:' in .coveragerc ignore: + - "Tests/32bit_segfault_check.py" - "Tests/bench_cffi_access.py" - "Tests/check_*.py" - "Tests/createfontdatachunk.py" diff --git a/docs/installation.rst b/docs/installation.rst index e29709bff..ed74dc147 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -285,8 +285,11 @@ Many of Pillow's features require external libraries: .. tab:: Windows using MSYS2/MinGW - To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 64-bit** console, - *not* **MSYS2** directly. + To build Pillow using MSYS2, make sure you run the **MSYS2 MinGW 32-bit** or + **MSYS2 MinGW 64-bit** console, *not* **MSYS2** directly. + + The following instructions target the 64-bit build, for 32-bit + replace all occurrences of ``mingw-w64-x86_64-`` with ``mingw-w64-i686-``. Make sure you have Python and GCC installed:: @@ -336,6 +339,8 @@ Many of Pillow's features require external libraries: pkg install -y python ndk-sysroot clang make \ libjpeg-turbo + This has been tested within the Termux app on ChromeOS, on x86. + Installing ^^^^^^^^^^ diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index d33b75e4d..b5edd0e36 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -4,11 +4,6 @@ Backwards Incompatible Changes ============================== -32-bit support -^^^^^^^^^^^^^^ - -32-bit architecture is no longer supported and 32-bit wheels are no longer provided. - Categories ^^^^^^^^^^ @@ -129,14 +124,6 @@ Image.coerce_e This undocumented method has been removed. -Deprecations -============ - -TODO -^^^^ - -TODO - API Changes =========== @@ -165,6 +152,11 @@ TODO Other Changes ============= +32-bit wheels +^^^^^^^^^^^^^ + +32-bit wheels are no longer provided. + Support display_jpeg() in IPython ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/setup.py b/setup.py index b647944d3..7c1ad6dc5 100755 --- a/setup.py +++ b/setup.py @@ -153,13 +153,16 @@ def _find_library_dirs_ldconfig(): ldconfig = "ldconfig" if shutil.which("ldconfig") else "/sbin/ldconfig" if sys.platform.startswith("linux") or sys.platform.startswith("gnu"): - machine = os.uname()[4] + if struct.calcsize("l") == 4: + machine = os.uname()[4] + "-32" + else: + machine = os.uname()[4] + "-64" mach_map = { - "x86_64": "libc6,x86-64", - "ppc64": "libc6,64bit", - "sparc64": "libc6,64bit", - "s390x": "libc6,64bit", - "ia64": "libc6,IA-64", + "x86_64-64": "libc6,x86-64", + "ppc64-64": "libc6,64bit", + "sparc64-64": "libc6,64bit", + "s390x-64": "libc6,64bit", + "ia64-64": "libc6,IA-64", } abi_type = mach_map.get(machine, "libc6") @@ -581,7 +584,10 @@ class pil_build_ext(build_ext): # user libs are at $PREFIX/lib _add_directory( library_dirs, - os.path.join(os.environ["ANDROID_ROOT"], "lib64"), + os.path.join( + os.environ["ANDROID_ROOT"], + "lib" if struct.calcsize("l") == 4 else "lib64", + ), ) elif sys.platform.startswith("netbsd"): diff --git a/src/libImaging/ImagingUtils.h b/src/libImaging/ImagingUtils.h index f2acabeac..0c0c1eda9 100644 --- a/src/libImaging/ImagingUtils.h +++ b/src/libImaging/ImagingUtils.h @@ -30,7 +30,7 @@ /* This is to work around a bug in GCC prior 4.9 in 64 bit mode. GCC generates code with partial dependency which is 3 times slower. See: https://stackoverflow.com/a/26588074/253146 */ -#if defined(__SSE__) && !defined(__NO_INLINE__) && \ +#if defined(__x86_64__) && defined(__SSE__) && !defined(__NO_INLINE__) && \ !defined(__clang__) && defined(GCC_VERSION) && (GCC_VERSION < 40900) static float __attribute__((always_inline)) inline _i2f(int v) { float x; diff --git a/winbuild/build.rst b/winbuild/build.rst index 97df950b3..99dfad301 100644 --- a/winbuild/build.rst +++ b/winbuild/build.rst @@ -27,7 +27,7 @@ Download and install: * `Ninja `_ (optional, use ``--nmake`` if not available; bundled in Visual Studio CMake component) -* x64: `Netwide Assembler (NASM) `_ +* x86/x64: `Netwide Assembler (NASM) `_ Any version of Visual Studio 2017 or newer should be supported, including Visual Studio 2017 Community, or Build Tools for Visual Studio 2019. @@ -42,7 +42,7 @@ Run ``build_prepare.py`` to configure the build:: usage: winbuild\build_prepare.py [-h] [-v] [-d PILLOW_BUILD] [--depends PILLOW_DEPS] - [--architecture {x64,ARM64}] + [--architecture {x86,x64,ARM64}] [--python PYTHON] [--executable EXECUTABLE] [--nmake] [--no-imagequant] [--no-fribidi] @@ -56,7 +56,7 @@ Run ``build_prepare.py`` to configure the build:: --depends PILLOW_DEPS directory used to store cached dependencies (default: 'winbuild\depends') - --architecture {x64,ARM64} + --architecture {x86,x64,ARM64} build architecture (default: same as host Python) --python PYTHON Python install directory (default: use host Python) --executable EXECUTABLE diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9984eb1bc..b9de071a0 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -3,6 +3,7 @@ import os import platform import re import shutil +import struct import subprocess import sys @@ -97,6 +98,7 @@ def cmd_msbuild( SF_PROJECTS = "https://sourceforge.net/projects" architectures = { + "x86": {"vcvars_arch": "x86", "msbuild_arch": "Win32"}, "x64": {"vcvars_arch": "x86_amd64", "msbuild_arch": "x64"}, "ARM64": {"vcvars_arch": "x86_arm64", "msbuild_arch": "ARM64"}, } @@ -609,7 +611,11 @@ if __name__ == "__main__": choices=architectures, default=os.environ.get( "ARCHITECTURE", - "ARM64" if platform.machine() == "ARM64" else "x64", + ( + "ARM64" + if platform.machine() == "ARM64" + else ("x86" if struct.calcsize("P") == 4 else "x64") + ), ), help="build architecture (default: same as host Python)", ) From c67d73d3c927081d5364b73b3711bf1980398fb4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 16:36:22 +1000 Subject: [PATCH 412/512] Test 32-bit Debian 12 --- .github/workflows/test-docker.yml | 1 + docs/installation.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index f22733dc4..36d9c131d 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -39,6 +39,7 @@ jobs: centos-stream-8-amd64, centos-stream-9-amd64, debian-11-bullseye-amd64, + debian-12-bookworm-x86, debian-12-bookworm-amd64, fedora-37-amd64, fedora-38-amd64, diff --git a/docs/installation.rst b/docs/installation.rst index ed74dc147..32676bf3f 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -453,7 +453,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 11 Bullseye | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Debian 12 Bookworm | 3.11 | x86-64 | +| Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ | Fedora 37 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ From 27b1acf4708dae6aaababebf993024e1744fd006 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Jun 2023 20:51:16 +1000 Subject: [PATCH 413/512] Test 32-bit Windows on AppVeyor --- .appveyor.yml | 4 ++-- docs/installation.rst | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 9a2eef767..36f5bd0ad 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,8 +10,8 @@ environment: TEST_OPTIONS: DEPLOY: YES matrix: - - PYTHON: C:/Python311-x64 - ARCHITECTURE: x64 + - PYTHON: C:/Python311 + ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 - PYTHON: C:/Python38-x64 ARCHITECTURE: x64 diff --git a/docs/installation.rst b/docs/installation.rst index 32676bf3f..5f3d2c9ef 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -477,6 +477,8 @@ These platforms are built and tested for every change. | Windows Server 2022 | 3.8, 3.9, 3.10, 3.11, | x86-64 | | | 3.12, PyPy3 | | | +----------------------------+---------------------+ +| | 3.11 | x86 | +| +----------------------------+---------------------+ | | 3.9 (MinGW) | x86-64 | | +----------------------------+---------------------+ | | 3.8, 3.9 (Cygwin) | x86-64 | From 1756f04acdd803a83b825cfa585527f0d78eab93 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 27 Jun 2023 21:19:32 +1000 Subject: [PATCH 414/512] Updated patch path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- winbuild/build_prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index a9b6450d8..dd20f351c 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -199,7 +199,7 @@ deps = { # link against webp.lib "#ifdef WEBP_SUPPORT": '#ifdef WEBP_SUPPORT\n#pragma comment(lib, "webp.lib")', # noqa: E501 }, - r"test/CMakeLists.txt": { + r"test\CMakeLists.txt": { "add_executable(test_write_read_tags ../placeholder.h)": "", "target_sources(test_write_read_tags PRIVATE test_write_read_tags.c)": "", # noqa: E501 "target_link_libraries(test_write_read_tags PRIVATE tiff)": "", From 8437d98f7fa9049f89aa501213f2e86f14dc07a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 11:43:05 +1000 Subject: [PATCH 415/512] Limit size even if one dimension is zero --- Tests/images/zero_width.gif | Bin 0 -> 44 bytes Tests/test_decompression_bomb.py | 9 +++++++++ src/PIL/Image.py | 2 +- src/_imagingft.c | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 Tests/images/zero_width.gif diff --git a/Tests/images/zero_width.gif b/Tests/images/zero_width.gif new file mode 100644 index 0000000000000000000000000000000000000000..da6823b60bb104bfd6969a93d71a0b8e245d836e GIT binary patch literal 44 pcmZ?wbhEHbWMEKW_{huv1pk5PKM?&_{K;|>B%lKk)MRDo2LK=z42A#z literal 0 HcmV?d00001 diff --git a/Tests/test_decompression_bomb.py b/Tests/test_decompression_bomb.py index 4fd02449c..87681a0b5 100644 --- a/Tests/test_decompression_bomb.py +++ b/Tests/test_decompression_bomb.py @@ -64,6 +64,15 @@ class TestDecompressionBomb: with pytest.raises(Image.DecompressionBombError): im.seek(1) + def test_exception_gif_zero_width(self): + # Set limit to trigger exception on the test file + Image.MAX_IMAGE_PIXELS = 4 * 64 * 128 + assert Image.MAX_IMAGE_PIXELS == 4 * 64 * 128 + + with pytest.raises(Image.DecompressionBombError): + with Image.open("Tests/images/zero_width.gif"): + pass + def test_exception_bmp(self): with pytest.raises(Image.DecompressionBombError): with Image.open("Tests/images/bmp/b/reallybig.bmp"): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 97f3f4926..400edcc5b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3141,7 +3141,7 @@ def _decompression_bomb_check(size): if MAX_IMAGE_PIXELS is None: return - pixels = size[0] * size[1] + pixels = max(1, size[0]) * max(1, size[1]) if pixels > 2 * MAX_IMAGE_PIXELS: msg = ( diff --git a/src/_imagingft.c b/src/_imagingft.c index 6cee021d4..fd3255244 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -880,7 +880,7 @@ font_render(FontObject *self, PyObject *args) { width += stroke_width * 2 + ceil(x_start); height += stroke_width * 2 + ceil(y_start); if (max_image_pixels != Py_None) { - if ((long long)width * height > PyLong_AsLongLong(max_image_pixels) * 2) { + if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); } From 811bfe3658bc5bb5d046ba805997312353ee95e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 17:57:02 +1000 Subject: [PATCH 416/512] Do not use CFFI access by default on PyPy --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 400edcc5b..0cc82cee3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -107,8 +107,7 @@ except ImportError as v: raise -# works everywhere, win for pypy, not cpython -USE_CFFI_ACCESS = hasattr(sys, "pypy_version_info") +USE_CFFI_ACCESS = False try: import cffi except ImportError: From 8a36b0fc2ddba222854f397e2066773a87b81efd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 20:10:10 +1000 Subject: [PATCH 417/512] Deprecate PyAccess --- Tests/test_image_access.py | 51 ++++++++++++++++++++---------------- docs/deprecations.rst | 9 +++++++ docs/releasenotes/10.0.0.rst | 10 +++++++ src/PIL/PyAccess.py | 3 +++ 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index af229d1a7..c9db3aee7 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -232,11 +232,13 @@ class TestImageGetPixel(AccessTest): assert im.convert("RGBA").getpixel((0, 0)) == (255, 0, 0, alpha) +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiPutPixel(TestImagePutPixel): _need_cffi_access = True +@pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.skipif(cffi is None, reason="No CFFI") class TestCffiGetPixel(TestImageGetPixel): _need_cffi_access = True @@ -252,7 +254,8 @@ class TestCffi(AccessTest): Using private interfaces, forcing a capi access and a pyaccess for the same image""" caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) w, h = im.size for x in range(0, w, 10): @@ -264,20 +267,16 @@ class TestCffi(AccessTest): access[(access.xsize + 1, access.ysize + 1)] def test_get_vs_c(self): - rgb = hopper("RGB") - rgb.load() - self._test_get_access(rgb) - self._test_get_access(hopper("RGBA")) - self._test_get_access(hopper("L")) - self._test_get_access(hopper("LA")) - self._test_get_access(hopper("1")) - self._test_get_access(hopper("P")) - # self._test_get_access(hopper('PA')) # PA -- how do I make a PA image? - self._test_get_access(hopper("F")) + with pytest.warns(DeprecationWarning): + rgb = hopper("RGB") + rgb.load() + self._test_get_access(rgb) + for mode in ("RGBA", "L", "LA", "1", "P", "F"): + self._test_get_access(hopper(mode)) - for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): - im = Image.new(mode, (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) # These don't actually appear to be modes that I can actually make, # as unpack sets them directly into the I mode. @@ -292,7 +291,8 @@ class TestCffi(AccessTest): Using private interfaces, forcing a capi access and a pyaccess for the same image""" caccess = im.im.pixel_access(False) - access = PyAccess.new(im, False) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) w, h = im.size for x in range(0, w, 10): @@ -301,13 +301,15 @@ class TestCffi(AccessTest): assert color == caccess[(x, y)] # Attempt to set the value on a read-only image - access = PyAccess.new(im, True) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, True) with pytest.raises(ValueError): access[(0, 0)] = color def test_set_vs_c(self): rgb = hopper("RGB") - rgb.load() + with pytest.warns(DeprecationWarning): + rgb.load() self._test_set_access(rgb, (255, 128, 0)) self._test_set_access(hopper("RGBA"), (255, 192, 128, 0)) self._test_set_access(hopper("L"), 128) @@ -326,6 +328,7 @@ class TestCffi(AccessTest): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_set_access(im, 2**13-1) + @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_not_implemented(self): assert PyAccess.new(hopper("BGR;15")) is None @@ -335,7 +338,8 @@ class TestCffi(AccessTest): for _ in range(10): # Do not save references to the image, only to the access object - px = Image.new("L", (size, 1), 0).load() + with pytest.warns(DeprecationWarning): + px = Image.new("L", (size, 1), 0).load() for i in range(size): # pixels can contain garbage if image is released assert px[i, 0] == 0 @@ -344,12 +348,13 @@ class TestCffi(AccessTest): def test_p_putpixel_rgb_rgba(self, mode): for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) - access = PyAccess.new(im, False) - access.putpixel((0, 0), color) + with pytest.warns(DeprecationWarning): + access = PyAccess.new(im, False) + access.putpixel((0, 0), color) - if len(color) == 3: - color += (255,) - assert im.convert("RGBA").getpixel((0, 0)) == color + if len(color) == 3: + color += (255,) + assert im.convert("RGBA").getpixel((0, 0)) == color class TestImagePutPixelError(AccessTest): diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 62687d869..b68663d35 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -22,6 +22,15 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. +PyAccess +~~~~~~~~ + +.. deprecated:: 10.0.0 + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). + Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index b5edd0e36..afb4164e9 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -124,6 +124,16 @@ Image.coerce_e This undocumented method has been removed. +Deprecations +============ + +PyAccess +^^^^^^^^ + +Since Pillow's C API is now faster than PyAccess on PyPy, +:py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow +11.0.0 (2024-10-15). + API Changes =========== diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 39747b4f3..99b46a4a6 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -22,6 +22,8 @@ import logging import sys +from ._deprecate import deprecate + try: from cffi import FFI @@ -47,6 +49,7 @@ logger = logging.getLogger(__name__) class PyAccess: def __init__(self, img, readonly=False): + deprecate("PyAccess", 11) vals = dict(img.im.unsafe_ptrs) self.readonly = readonly self.image8 = ffi.cast("unsigned char **", vals["image8"]) From 1a185973dd173e07ec9d2c125ff1980844d996f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Jun 2023 22:00:32 +1000 Subject: [PATCH 418/512] Mention default behaviour change --- docs/deprecations.rst | 2 +- docs/releasenotes/10.0.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index b68663d35..c79b89663 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -29,7 +29,7 @@ PyAccess Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index afb4164e9..94761efaf 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -132,7 +132,7 @@ PyAccess Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow -11.0.0 (2024-10-15). +11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. API Changes =========== From b5ce3193b68569baee207768bade77b0fb1643e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 08:30:10 +1000 Subject: [PATCH 419/512] Deprecate Image.USE_CFFI_ACCESS --- docs/deprecations.rst | 7 +++++-- docs/releasenotes/10.0.0.rst | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index c79b89663..ce956cade 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -22,8 +22,8 @@ be removed in Pillow 11 (2024-10-15). This class was only made as a helper to be used internally, so there is no replacement. If you need this functionality though, it is a very short class that can easily be recreated in your own code. -PyAccess -~~~~~~~~ +PyAccess and Image.USE_CFFI_ACCESS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 10.0.0 @@ -31,6 +31,9 @@ Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + Removed features ---------------- diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 94761efaf..9b92e27d8 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -127,13 +127,16 @@ This undocumented method has been removed. Deprecations ============ -PyAccess -^^^^^^^^ +PyAccess and Image.USE_CFFI_ACCESS +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Since Pillow's C API is now faster than PyAccess on PyPy, :py:mod:`~PIL.PyAccess` has been deprecated and will be removed in Pillow 11.0.0 (2024-10-15). Pillow's C API will now be used by default on PyPy instead. +``Image.USE_CFFI_ACCESS``, for switching from the C API to PyAccess, is +similarly deprecated. + API Changes =========== From 456ef61cb58219daafd68ed82fedf849e8ca0d80 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 09:43:06 +1000 Subject: [PATCH 420/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9d95f3cd2..af695bcb0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,8 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- -- Removed support for 32-bit #7228 - [radarhere, hugovk] +- Limit size even if one dimension is zero in decompression bomb check #7235 + [radarhere] - Use --config-settings instead of deprecated --global-option #7171 [radarhere] From 444b8118bdccb021ca37df4c173789b31e77e8ea Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 15:11:16 +1000 Subject: [PATCH 421/512] Updated libwebp to 1.3.1 --- depends/install_webp.sh | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/depends/install_webp.sh b/depends/install_webp.sh index f8b985a7a..4636aab43 100755 --- a/depends/install_webp.sh +++ b/depends/install_webp.sh @@ -1,7 +1,7 @@ #!/bin/bash # install webp -archive=libwebp-1.3.0 +archive=libwebp-1.3.1 ./download-and-extract.sh $archive https://raw.githubusercontent.com/python-pillow/pillow-depends/main/$archive.tar.gz diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index b9de071a0..f1f20422b 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -165,9 +165,9 @@ deps = { "libs": [r"liblzma.lib"], }, "libwebp": { - "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.0.tar.gz", - "filename": "libwebp-1.3.0.tar.gz", - "dir": "libwebp-1.3.0", + "url": "http://downloads.webmproject.org/releases/webp/libwebp-1.3.1.tar.gz", + "filename": "libwebp-1.3.1.tar.gz", + "dir": "libwebp-1.3.1", "license": "COPYING", "build": [ cmd_rmdir(r"output\release-static"), # clean From ae43cda4c5c5c3935624a98258571040a71e026b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 20:58:43 +1000 Subject: [PATCH 422/512] Added release notes for #7235 --- docs/releasenotes/10.0.0.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 9b92e27d8..01b15f386 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -157,10 +157,15 @@ TODO Security ======== -TODO -^^^^ +Limit size even if one dimension is zero +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +When performing decompression bomb checks, Pillow did not reject images with +excessive width and zero height, or zero width and excessive height. That has +now been fixed. + +This effectively dates to the PIL fork, since problem images would still have +been processed before Pillow started checking for decompression bombs. Other Changes ============= From 49cde0ad3d1234cf14db2d2159ab3b95e7e929d5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 29 Jun 2023 21:03:42 +1000 Subject: [PATCH 423/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index af695bcb0..c6b6305d8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Do not use CFFI access by default on PyPy #7236 + [radarhere] + - Limit size even if one dimension is zero in decompression bomb check #7235 [radarhere] From 07404991512d370723ef835b0954d2fcb4eee883 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 09:05:59 +1000 Subject: [PATCH 424/512] Prioritise speed in _repr_png_ --- src/PIL/Image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0464513fd..08bb5615b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -632,7 +632,7 @@ class Image: ) ) - def _repr_image(self, image_format): + def _repr_image(self, image_format, **kwargs): """Helper function for iPython display hook. :param image_format: Image format. @@ -640,7 +640,7 @@ class Image: """ b = io.BytesIO() try: - self.save(b, image_format) + self.save(b, image_format, **kwargs) except Exception as e: msg = f"Could not save to {image_format} for display" raise ValueError(msg) from e @@ -651,7 +651,7 @@ class Image: :returns: PNG version of the image as bytes """ - return self._repr_image("PNG") + return self._repr_image("PNG", compress_level=1) def _repr_jpeg_(self): """iPython display hook support for JPEG format. From 0fb69fa821155c1b213f3f3488d3057b6ba7c154 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 16:59:36 +1000 Subject: [PATCH 425/512] Added release notes for #7123 --- docs/releasenotes/10.0.0.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 01b15f386..94ff04d46 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -149,10 +149,13 @@ An optional line ``width`` parameter has been added to API Additions ============= -TODO -^^^^ +Added ``alpha_only`` argument to ``getbbox()`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +:py:meth:`~PIL.Image.Image.getbbox` now accepts a keyword argument of +``alpha_only``. This is an optional flag, defaulting to ``True``. If ``True`` +and the image has an alpha channel, trim transparent pixels. Otherwise, trim +pixels when all channels are zero. Security ======== From 1fe1bb49c452b0318cad12ea9d97c3bef188e9a7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 30 Jun 2023 23:32:26 +1000 Subject: [PATCH 426/512] Added ImageFont.MAX_STRING_LENGTH --- Tests/test_imagefont.py | 19 +++++++++++++++++++ docs/reference/ImageFont.rst | 18 ++++++++++++++++++ docs/releasenotes/10.0.0.rst | 12 ++++++++++++ src/PIL/ImageFont.py | 15 +++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 7fa8ff8cb..c50447a15 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1038,6 +1038,25 @@ def test_render_mono_size(): assert_image_equal_tofile(im, "Tests/images/text_mono.gif") +def test_too_many_characters(font): + with pytest.raises(ValueError): + font.getlength("A" * 1000001) + with pytest.raises(ValueError): + font.getbbox("A" * 1000001) + with pytest.raises(ValueError): + font.getmask2("A" * 1000001) + + transposed_font = ImageFont.TransposedFont(font) + with pytest.raises(ValueError): + transposed_font.getlength("A" * 1000001) + + default_font = ImageFont.load_default() + with pytest.raises(ValueError): + default_font.getlength("A" * 1000001) + with pytest.raises(ValueError): + default_font.getbbox("A" * 1000001) + + @pytest.mark.parametrize( "test_file", [ diff --git a/docs/reference/ImageFont.rst b/docs/reference/ImageFont.rst index 946bd3c4b..2abfa0cc9 100644 --- a/docs/reference/ImageFont.rst +++ b/docs/reference/ImageFont.rst @@ -18,6 +18,15 @@ OpenType fonts (as well as other font formats supported by the FreeType library). For earlier versions, TrueType support is only available as part of the imToolkit package. +.. warning:: + To protect against potential DOS attacks when using arbitrary strings as + text input, Pillow will raise a ``ValueError`` if the number of characters + is over a certain limit, :py:data:`MAX_STRING_LENGTH`. + + This threshold can be changed by setting + :py:data:`MAX_STRING_LENGTH`. It can be disabled by setting + ``ImageFont.MAX_STRING_LENGTH = None``. + Example ------- @@ -73,3 +82,12 @@ Constants Requires Raqm, you can check support using :py:func:`PIL.features.check_feature` with ``feature="raqm"``. + +Constants +--------- + +.. data:: MAX_STRING_LENGTH + + Set to 1,000,000, to protect against potential DOS attacks. Pillow will + raise a ``ValueError`` if the number of characters is over this limit. The + check can be disabled by setting ``ImageFont.MAX_STRING_LENGTH = None``. diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 94ff04d46..4cd629322 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -170,6 +170,18 @@ now been fixed. This effectively dates to the PIL fork, since problem images would still have been processed before Pillow started checking for decompression bombs. +Added ImageFont.MAX_STRING_LENGTH +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To protect against potential DOS attacks when using arbitrary strings as text +input, Pillow will now raise a ``ValueError`` if the number of characters +passed into ImageFont methods is over a certain limit, +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. + +This threshold can be changed by setting +:py:data:`PIL.ImageFont.MAX_STRING_LENGTH`. It can be disabled by setting +``ImageFont.MAX_STRING_LENGTH = None``. + Other Changes ============= diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 3ddc1aaad..1030985eb 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -41,6 +41,9 @@ class Layout(IntEnum): RAQM = 1 +MAX_STRING_LENGTH = 1000000 + + try: from . import _imagingft as core except ImportError as ex: @@ -49,6 +52,12 @@ except ImportError as ex: core = DeferredError(ex) +def _string_length_check(text): + if MAX_STRING_LENGTH is not None and len(text) > MAX_STRING_LENGTH: + msg = "too many characters in string" + raise ValueError(msg) + + # FIXME: add support for pilfont2 format (see FontFile.py) # -------------------------------------------------------------------- @@ -152,6 +161,7 @@ class ImageFont: :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) width, height = self.font.getsize(text) return 0, 0, width, height @@ -162,6 +172,7 @@ class ImageFont: .. versionadded:: 9.2.0 """ + _string_length_check(text) width, height = self.font.getsize(text) return width @@ -309,6 +320,7 @@ class FreeTypeFont: :return: Width for horizontal, height for vertical text. """ + _string_length_check(text) return self.font.getlength(text, mode, direction, features, language) / 64 def getbbox( @@ -368,6 +380,7 @@ class FreeTypeFont: :return: ``(left, top, right, bottom)`` bounding box """ + _string_length_check(text) size, offset = self.font.getsize( text, mode, direction, features, language, anchor ) @@ -546,6 +559,7 @@ class FreeTypeFont: :py:mod:`PIL.Image.core` interface module, and the text offset, the gap between the starting coordinate and the first marking """ + _string_length_check(text) if start is None: start = (0, 0) im, size, offset = self.font.render( @@ -684,6 +698,7 @@ class TransposedFont: if self.orientation in (Image.Transpose.ROTATE_90, Image.Transpose.ROTATE_270): msg = "text length is undefined for text rotated by 90 or 270 degrees" raise ValueError(msg) + _string_length_check(text) return self.font.getlength(text, *args, **kwargs) From d398fedb9d5af22316c715d2066176d15031d439 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Jul 2023 07:25:18 +1000 Subject: [PATCH 427/512] Added underscores for readability Co-authored-by: Hugo van Kemenade --- Tests/test_imagefont.py | 12 ++++++------ src/PIL/ImageFont.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index c50447a15..02622e721 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -1040,21 +1040,21 @@ def test_render_mono_size(): def test_too_many_characters(font): with pytest.raises(ValueError): - font.getlength("A" * 1000001) + font.getlength("A" * 1_000_001) with pytest.raises(ValueError): - font.getbbox("A" * 1000001) + font.getbbox("A" * 1_000_001) with pytest.raises(ValueError): - font.getmask2("A" * 1000001) + font.getmask2("A" * 1_000_001) transposed_font = ImageFont.TransposedFont(font) with pytest.raises(ValueError): - transposed_font.getlength("A" * 1000001) + transposed_font.getlength("A" * 1_000_001) default_font = ImageFont.load_default() with pytest.raises(ValueError): - default_font.getlength("A" * 1000001) + default_font.getlength("A" * 1_000_001) with pytest.raises(ValueError): - default_font.getbbox("A" * 1000001) + default_font.getbbox("A" * 1_000_001) @pytest.mark.parametrize( diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 1030985eb..b7d40208c 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -41,7 +41,7 @@ class Layout(IntEnum): RAQM = 1 -MAX_STRING_LENGTH = 1000000 +MAX_STRING_LENGTH = 1_000_000 try: From 8c1dc819fd91471825da01976ac0e0bc8789590f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 1 Jul 2023 10:31:34 +1000 Subject: [PATCH 428/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c6b6305d8..8ec5d33ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,24 @@ Changelog (Pillow) 10.0.0 (unreleased) ------------------- +- Added ImageFont.MAX_STRING_LENGTH #7244 + [radarhere, hugovk] + +- Fix Windows build with pyproject.toml #7230 + [hugovk, nulano, radarhere] + +- Do not close provided file handles with libtiff #7199 + [radarhere] + +- Convert to HSV if mode is HSV in getcolor() #7226 + [radarhere] + +- Added alpha_only argument to getbbox() #7123 + [radarhere. hugovk] + +- Prioritise speed in _repr_png_ #7242 + [radarhere] + - Do not use CFFI access by default on PyPy #7236 [radarhere] From 39a3b1d83edcf826c3864e26bedff5b4e4dd331b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Jul 2023 18:09:27 +1000 Subject: [PATCH 429/512] Fixed deallocating mask images --- src/PIL/ImageFont.py | 12 ++++++++++-- src/_imagingft.c | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index b7d40208c..05828a72f 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -562,9 +562,17 @@ class FreeTypeFont: _string_length_check(text) if start is None: start = (0, 0) - im, size, offset = self.font.render( + im = None + + def fill(mode, size): + nonlocal im + + im = Image.core.fill(mode, size) + return im + + size, offset = self.font.render( text, - Image.core.fill, + fill, mode, direction, features, diff --git a/src/_imagingft.c b/src/_imagingft.c index fd3255244..62819a569 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -882,7 +882,7 @@ font_render(FontObject *self, PyObject *args) { if (max_image_pixels != Py_None) { if ((long long)(width > 1 ? width : 1) * (height > 1 ? height : 1) > PyLong_AsLongLong(max_image_pixels) * 2) { PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", Py_None, width, height, 0, 0); + return Py_BuildValue("(ii)(ii)", width, height, 0, 0); } } @@ -898,7 +898,7 @@ font_render(FontObject *self, PyObject *args) { y_offset -= stroke_width; if (count == 0 || width == 0 || height == 0) { PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); } if (stroke_width) { @@ -1113,9 +1113,10 @@ font_render(FontObject *self, PyObject *args) { if (bitmap_converted_ready) { FT_Bitmap_Done(library, &bitmap_converted); } + Py_DECREF(image); FT_Stroker_Done(stroker); PyMem_Del(glyph_info); - return Py_BuildValue("O(ii)(ii)", image, width, height, x_offset, y_offset); + return Py_BuildValue("(ii)(ii)", width, height, x_offset, y_offset); glyph_error: if (im->destroy) { From 6e28ed1f36d0eb74053af54e1eddc9c29db698cd Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 1 Jul 2023 22:08:11 +1000 Subject: [PATCH 430/512] 10.0.0 version bump --- CHANGES.rst | 5 ++++- src/PIL/_version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8ec5d33ed..94cd6e7bc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,9 +2,12 @@ Changelog (Pillow) ================== -10.0.0 (unreleased) +10.0.0 (2023-07-01) ------------------- +- Fixed deallocating mask images #7246 + [radarhere] + - Added ImageFont.MAX_STRING_LENGTH #7244 [radarhere, hugovk] diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 800203d51..1fc7f7334 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "10.0.0.dev0" +__version__ = "10.0.0" From 1ffe3354d7332e50c6b87a9f352698b3eea0e98c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 2 Jul 2023 00:59:40 +1000 Subject: [PATCH 431/512] 10.1.0.dev0 version bump --- src/PIL/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_version.py b/src/PIL/_version.py index 1fc7f7334..cf5019e80 100644 --- a/src/PIL/_version.py +++ b/src/PIL/_version.py @@ -1,2 +1,2 @@ # Master version for Pillow -__version__ = "10.0.0" +__version__ = "10.1.0.dev0" From d56fb2435df5121bfe9c88a6143365737d284082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 2 Jul 2023 04:13:02 +0200 Subject: [PATCH 432/512] Remove redundant wheel dep from pyproject.toml Remove the redundant `wheel` dependency, as it is added by the backend automatically. Listing it explicitly in the documentation was a historical mistake and has been fixed since, see: https://github.com/pypa/setuptools/commit/f7d30a9529378cf69054b5176249e5457aaf640a While Pillow uses a custom backend that modifies the `bdist_wheel` method, it does not import `wheel` or use it in a way that would rely on setuptools implementation details. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 93a433608..fd9c05f92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,6 @@ build-backend = "backend" requires = [ "setuptools>=67.8", - "wheel", ] backend-path = [ "_custom_build", From e7398c7888f057c91855ee820cf6649033ceeac3 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 2 Jul 2023 00:52:08 -0500 Subject: [PATCH 433/512] update PyTypeObject slot names tp_print was renamed to tp_vectorcall_offset with Python 3.8, and tp_compare was renamed to tp_as_async with Python 3.5. tp_size has always been tp_basicsize; I don't know why that one was wrong. --- src/_imaging.c | 52 +++++++++++++++++++++++------------------------ src/_imagingcms.c | 32 ++++++++++++++--------------- src/_imagingft.c | 18 ++++++++-------- src/_webp.c | 24 +++++++++++----------- src/decode.c | 12 +++++------ src/display.c | 12 +++++------ src/encode.c | 12 +++++------ src/outline.c | 12 +++++------ src/path.c | 12 +++++------ 9 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 7b4174d6f..e15cb89fc 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -3725,18 +3725,18 @@ static PySequenceMethods image_as_sequence = { static PyTypeObject Imaging_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingCore", /*tp_name*/ - sizeof(ImagingObject), /*tp_size*/ + sizeof(ImagingObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - &image_as_sequence, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + &image_as_sequence, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3760,18 +3760,18 @@ static PyTypeObject Imaging_Type = { static PyTypeObject ImagingFont_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingFont", /*tp_name*/ - sizeof(ImagingFontObject), /*tp_size*/ + sizeof(ImagingFontObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_font_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3793,18 +3793,18 @@ static PyTypeObject ImagingFont_Type = { static PyTypeObject ImagingDraw_Type = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDraw", /*tp_name*/ - sizeof(ImagingDrawObject), /*tp_size*/ + sizeof(ImagingDrawObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_draw_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -3835,19 +3835,19 @@ static PyMappingMethods pixel_access_as_mapping = { /* type description */ static PyTypeObject PixelAccess_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", - sizeof(PixelAccessObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "PixelAccess", /*tp_name*/ + sizeof(PixelAccessObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)pixel_access_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - &pixel_access_as_mapping, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + &pixel_access_as_mapping, /*tp_as_mapping*/ 0 /*tp_hash*/ }; diff --git a/src/_imagingcms.c b/src/_imagingcms.c index ddfe6ad64..56d5d73f8 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1421,19 +1421,19 @@ static struct PyGetSetDef cms_profile_getsetters[] = { {NULL}}; static PyTypeObject CmsProfile_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "PIL._imagingcms.CmsProfile", /*tp_name */ - sizeof(CmsProfileObject), - 0, /*tp_basicsize, tp_itemsize */ + PyVarObject_HEAD_INIT(NULL, 0) "PIL._imagingcms.CmsProfile", /*tp_name*/ + sizeof(CmsProfileObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)cms_profile_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -1473,19 +1473,19 @@ static struct PyGetSetDef cms_transform_getsetters[] = { {NULL}}; static PyTypeObject CmsTransform_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "CmsTransform", - sizeof(CmsTransformObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "CmsTransform", /*tp_name*/ + sizeof(CmsTransformObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ (destructor)cms_transform_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/_imagingft.c b/src/_imagingft.c index 62819a569..2165fbc7a 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1401,19 +1401,19 @@ static struct PyGetSetDef font_getsetters[] = { {NULL}}; static PyTypeObject Font_Type = { - PyVarObject_HEAD_INIT(NULL, 0) "Font", - sizeof(FontObject), - 0, + PyVarObject_HEAD_INIT(NULL, 0) "Font", /*tp_name*/ + sizeof(FontObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ /* methods */ - (destructor)font_dealloc, /* tp_dealloc */ - 0, /* tp_print */ + (destructor)font_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/_webp.c b/src/_webp.c index fe63027fb..a1b4dbc1a 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -487,18 +487,18 @@ static struct PyMethodDef _anim_encoder_methods[] = { // WebPAnimEncoder type definition static PyTypeObject WebPAnimEncoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimEncoder", /*tp_name */ - sizeof(WebPAnimEncoderObject), /*tp_size */ + sizeof(WebPAnimEncoderObject), /*tp_basicsize */ 0, /*tp_itemsize */ /* methods */ (destructor)_anim_encoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ @@ -530,18 +530,18 @@ static struct PyMethodDef _anim_decoder_methods[] = { // WebPAnimDecoder type definition static PyTypeObject WebPAnimDecoder_Type = { PyVarObject_HEAD_INIT(NULL, 0) "WebPAnimDecoder", /*tp_name */ - sizeof(WebPAnimDecoderObject), /*tp_size */ + sizeof(WebPAnimDecoderObject), /*tp_basicsize */ 0, /*tp_itemsize */ /* methods */ (destructor)_anim_decoder_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/decode.c b/src/decode.c index 7e3fadc04..ea2f3af80 100644 --- a/src/decode.c +++ b/src/decode.c @@ -258,18 +258,18 @@ static struct PyGetSetDef getseters[] = { static PyTypeObject ImagingDecoderType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDecoder", /*tp_name*/ - sizeof(ImagingDecoderObject), /*tp_size*/ + sizeof(ImagingDecoderObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/display.c b/src/display.c index 754a6ae78..ef2ff3754 100644 --- a/src/display.c +++ b/src/display.c @@ -251,18 +251,18 @@ static struct PyGetSetDef getsetters[] = { static PyTypeObject ImagingDisplayType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingDisplay", /*tp_name*/ - sizeof(ImagingDisplayObject), /*tp_size*/ + sizeof(ImagingDisplayObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_delete, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/encode.c b/src/encode.c index 308bd2059..08544aede 100644 --- a/src/encode.c +++ b/src/encode.c @@ -323,18 +323,18 @@ static struct PyGetSetDef getseters[] = { static PyTypeObject ImagingEncoderType = { PyVarObject_HEAD_INIT(NULL, 0) "ImagingEncoder", /*tp_name*/ - sizeof(ImagingEncoderObject), /*tp_size*/ + sizeof(ImagingEncoderObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/outline.c b/src/outline.c index 0a9a3646e..27cc255cf 100644 --- a/src/outline.c +++ b/src/outline.c @@ -155,18 +155,18 @@ static struct PyMethodDef _outline_methods[] = { static PyTypeObject OutlineType = { PyVarObject_HEAD_INIT(NULL, 0) "Outline", /*tp_name*/ - sizeof(OutlineObject), /*tp_size*/ + sizeof(OutlineObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)_outline_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - 0, /*tp_as_sequence */ - 0, /*tp_as_mapping */ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ diff --git a/src/path.c b/src/path.c index e17580fa2..cc0698c4d 100644 --- a/src/path.c +++ b/src/path.c @@ -583,18 +583,18 @@ static PyMappingMethods path_as_mapping = { static PyTypeObject PyPathType = { PyVarObject_HEAD_INIT(NULL, 0) "Path", /*tp_name*/ - sizeof(PyPathObject), /*tp_size*/ + sizeof(PyPathObject), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ (destructor)path_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_vectorcall_offset*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ - 0, /*tp_compare*/ + 0, /*tp_as_async*/ 0, /*tp_repr*/ - 0, /*tp_as_number */ - &path_as_sequence, /*tp_as_sequence */ - &path_as_mapping, /*tp_as_mapping */ + 0, /*tp_as_number*/ + &path_as_sequence, /*tp_as_sequence*/ + &path_as_mapping, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call*/ 0, /*tp_str*/ From 9e31a677b90f2d3c80868df63531adcd485c8224 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Jul 2023 19:29:28 +0000 Subject: [PATCH 434/512] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/asottile/yesqa: v1.4.0 → v1.5.0](https://github.com/asottile/yesqa/compare/v1.4.0...v1.5.0) - [github.com/tox-dev/tox-ini-fmt: 1.3.0 → 1.3.1](https://github.com/tox-dev/tox-ini-fmt/compare/1.3.0...1.3.1) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 872c73843..320a77f55 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: files: ^src/ - repo: https://github.com/asottile/yesqa - rev: v1.4.0 + rev: v1.5.0 hooks: - id: yesqa @@ -65,7 +65,7 @@ repos: - id: validate-pyproject - repo: https://github.com/tox-dev/tox-ini-fmt - rev: 1.3.0 + rev: 1.3.1 hooks: - id: tox-ini-fmt From 082c43656dbab902e859b055b8471ec839f17807 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Jul 2023 08:29:17 +1000 Subject: [PATCH 435/512] Updated libjpeg-turbo to 3.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 5a5bb8e0a..0b7e5c824 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -106,9 +106,9 @@ architectures = { deps = { "libjpeg": { "url": SF_PROJECTS - + "/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", + + "/libjpeg-turbo/files/3.0.0/libjpeg-turbo-3.0.0.tar.gz/download", + "filename": "libjpeg-turbo-3.0.0.tar.gz", + "dir": "libjpeg-turbo-3.0.0", "license": ["README.ijg", "LICENSE.md"], "license_pattern": ( "(LEGAL ISSUES\n============\n\n.+?)\n\nREFERENCES\n==========" From 5a9c2321b4767decd8a3bf62936b25fe43e5593c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 4 Jul 2023 22:39:41 +1000 Subject: [PATCH 436/512] Updated libjpeg shared library name --- Tests/oss-fuzz/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/oss-fuzz/build.sh b/Tests/oss-fuzz/build.sh index 7e9098f53..37fad7bc8 100755 --- a/Tests/oss-fuzz/build.sh +++ b/Tests/oss-fuzz/build.sh @@ -20,7 +20,7 @@ python3 setup.py build --build-base=/tmp/build install # Build fuzzers in $OUT. for fuzzer in $(find $SRC -name 'fuzz_*.py'); do compile_python_fuzzer $fuzzer \ - --add-binary /usr/local/lib/libjpeg.so.62.3.0:. \ + --add-binary /usr/local/lib/libjpeg.so.62.4.0:. \ --add-binary /usr/local/lib/libfreetype.so.6:. \ --add-binary /usr/local/lib/liblcms2.so.2:. \ --add-binary /usr/local/lib/libopenjp2.so.7:. \ From 9a32c0f821b2a6f480c3d5fd6af0010551c6cea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Jadi?= Date: Wed, 5 Jul 2023 11:26:46 +0200 Subject: [PATCH 437/512] doc WAL: Add link to PIL.Image.Image.putpalette [ci skip] --- 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 bbcf48e42..fbaca5ae7 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1404,7 +1404,7 @@ the open function in the :py:mod:`~PIL.WalImageFile` module to read files in this format. By default, a Quake2 standard palette is attached to the texture. To override -the palette, use the putpalette method. +the palette, use the :py:func:`PIL.Image.Image.putpalette()` method. WMF, EMF ^^^^^^^^ From e6beb815234965f07b3c8e671066c196cd07ddd5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 6 Jul 2023 13:22:50 +1000 Subject: [PATCH 438/512] Updated macOS tested Pillow versions --- docs/installation.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 5c4872b83..a4d22316d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -499,7 +499,9 @@ These platforms have been reported to work at the versions mentioned. | Operating system | | Tested Python | | Latest tested | | Tested | | | | versions | | Pillow version | | processors | +==================================+===========================+==================+==============+ -| macOS 13 Ventura | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 |arm | +| macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm | +| +---------------------------+------------------+ | +| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ From 6d440ac995e62223878de89ac46b14ba63b6a9aa Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jul 2023 08:42:37 -0400 Subject: [PATCH 439/512] Handle exceptions in _repr_jpeg_ and _repr_png_ In 10.0.0 a _repr_jpeg_ implementation was added to the Image object to enable the use of display_jpeg() in IPython environments. However, in some cases the implementation of this method could result in an exception being raised while trying to generate the jpeg data. The best example is if the image data is in an RGBA format as a result of the object being created by opening a PNG file. In this case trying to save the Image object as a jpeg will error because the jpeg format can't represent the transparency in the alpha channel. This results in an exception being raised in the IPython/Jupyter context when outputing the image object. However, in cases like this IPython allows the repr methods to return None to indicate there is no representation in that format available. [1] This commit updates the _repr_png_ and _repr_jpeg_ methods to catch any exception that might be raised while trying to generate the image data. If an exception is raised we treat that as not being able to generate image data in that format and return None instead. Related to #7259 [1] https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods --- src/PIL/Image.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a519a28af..6501c2355 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -651,14 +651,20 @@ class Image: :returns: PNG version of the image as bytes """ - return self._repr_image("PNG", compress_level=1) + try: + return self._repr_image("PNG", compress_level=1) + except Exception: + return None def _repr_jpeg_(self): """iPython display hook support for JPEG format. :returns: JPEG version of the image as bytes """ - return self._repr_image("JPEG") + try: + self._repr_image("JPEG") + except Exception: + return None @property def __array_interface__(self): From 9517feccd9a0f998342e4527c24b4100907b1bb8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jul 2023 09:00:00 -0400 Subject: [PATCH 440/512] Update src/PIL/Image.py Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- 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 6501c2355..4b088bdfd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -662,7 +662,7 @@ class Image: :returns: JPEG version of the image as bytes """ try: - self._repr_image("JPEG") + return self._repr_image("JPEG") except Exception: return None From 6215cd3e0fe1cc8bbecb5173badcd13e85135b2f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 6 Jul 2023 09:28:35 -0400 Subject: [PATCH 441/512] Update tests to handle no longer raising --- Tests/test_file_jpeg.py | 5 ++--- Tests/test_file_png.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 0247527f5..1dc82ef1e 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -929,11 +929,10 @@ class TestFileJpeg: assert repr_jpeg.format == "JPEG" assert_image_similar(im, repr_jpeg, 17) - def test_repr_jpeg_error(self): + def test_repr_jpeg_error_returns_none(self): im = hopper("F") - with pytest.raises(ValueError): - im._repr_jpeg_() + assert im._repr_jpeg_() is None @pytest.mark.skipif(not is_win32(), reason="Windows only") diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index c4db97905..3ffe93c6d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -532,11 +532,10 @@ class TestFilePng: assert repr_png.format == "PNG" assert_image_equal(im, repr_png) - def test_repr_png_error(self): + def test_repr_png_error_returns_none(self): im = hopper("F") - with pytest.raises(ValueError): - im._repr_png_() + assert im._repr_png_() is None def test_chunk_order(self, tmp_path): with Image.open("Tests/images/icc_profile.png") as im: From d17947e802498a1315ea070cb0df5f01cb5c43c0 Mon Sep 17 00:00:00 2001 From: Rudi Heitbaum Date: Sat, 8 Jul 2023 12:39:40 +0000 Subject: [PATCH 442/512] Fix missing symbols as libtiff can depend on libjpeg when compiling Pillow with libtiff and libjpeg (with jpeg12 enabled - which is the default with libjpeg-3.0.0) the libtiff object tif_jpeg_12.c.o uses the following libjpeg12 functions: jpeg12_read_raw_data, jpeg12_read_scanlines, jpeg12_write_raw_data, jpeg12_write_scanlines. update the ordering of libs.append(feature.tiff) to be before libs.append(feature.jpeg) to allow the linker to include the required functions. this issue occurs when the libtiff and libjpeg libraries are static (not shared.) Signed-off-by: Rudi Heitbaum --- setup.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 024634ad8..baf7b9395 100755 --- a/setup.py +++ b/setup.py @@ -816,6 +816,15 @@ class pil_build_ext(build_ext): libs = self.add_imaging_libs.split() defs = [] + if feature.tiff: + libs.append(feature.tiff) + defs.append(("HAVE_LIBTIFF", None)) + if sys.platform == "win32": + # This define needs to be defined if-and-only-if it was defined + # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, + # so we have to guess; by default it is defined in all Windows builds. + # See #4237, #5243, #5359 for more information. + defs.append(("USE_WIN32_FILEIO", None)) if feature.jpeg: libs.append(feature.jpeg) defs.append(("HAVE_LIBJPEG", None)) @@ -830,15 +839,6 @@ class pil_build_ext(build_ext): if feature.imagequant: libs.append(feature.imagequant) defs.append(("HAVE_LIBIMAGEQUANT", None)) - if feature.tiff: - libs.append(feature.tiff) - defs.append(("HAVE_LIBTIFF", None)) - if sys.platform == "win32": - # This define needs to be defined if-and-only-if it was defined - # when compiling LibTIFF. LibTIFF doesn't expose it in `tiffconf.h`, - # so we have to guess; by default it is defined in all Windows builds. - # See #4237, #5243, #5359 for more information. - defs.append(("USE_WIN32_FILEIO", None)) if feature.xcb: libs.append(feature.xcb) defs.append(("HAVE_XCB", None)) From 1953e4353874fc6da9445022adeaf41008e89201 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 9 Jul 2023 15:00:41 +1000 Subject: [PATCH 443/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 94cd6e7bc..41faa1310 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,12 @@ Changelog (Pillow) ================== +10.1.0 (unreleased) +------------------- + +- Fix missing symbols when libtiff depends on libjpeg #7270 + [heitbaum] + 10.0.0 (2023-07-01) ------------------- From 16f5c6d5d762a11c67918a7efd0d3d38c7475098 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 9 Jul 2023 21:51:42 +1000 Subject: [PATCH 444/512] Updated harfbuzz to 8.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 0b7e5c824..df834a387 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -335,9 +335,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/7.3.0.zip", - "filename": "harfbuzz-7.3.0.zip", - "dir": "harfbuzz-7.3.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.0.0.zip", + "filename": "harfbuzz-8.0.0.zip", + "dir": "harfbuzz-8.0.0", "license": "COPYING", "build": [ *cmds_cmake( From ab990fab77e1ee7001399f239c08fab1cf617635 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Wed, 12 Jul 2023 16:31:45 +0300 Subject: [PATCH 445/512] Add support for Python 3.12 --- docs/newer-versions.csv | 15 ++++++++------- setup.cfg | 1 + tox.ini | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index d53947ff5..c228b7c29 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,7 +1,8 @@ -Python,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 10,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,Yes,Yes,Yes,Yes,, -Pillow 8.3.2 - 8.4,,Yes,Yes,Yes,Yes,Yes, -Pillow 8.0 - 8.3.1,,,Yes,Yes,Yes,Yes, -Pillow 7.0 - 7.2,,,,Yes,Yes,Yes,Yes +Python,3.12,3.11,3.1,3.9,3.8,3.7,3.6,3.5 +Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,Yes,Yes,Yes,Yes,, +Pillow 8.3.2 - 8.4,,,Yes,Yes,Yes,Yes,Yes, +Pillow 8.0 - 8.3.1,,,,Yes,Yes,Yes,Yes, +Pillow 7.0 - 7.2,,,,,Yes,Yes,Yes,Yes \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 06e95d7cc..e560f9516 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Multimedia :: Graphics diff --git a/tox.ini b/tox.ini index a79089f51..5388ed243 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 311, 310, 39, 38} + py{py3, 312, 311, 310, 39, 38} [testenv] deps = From a682ceaf47abbe28dc70c6bd4aab06f8f3f4ac90 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Jul 2023 15:20:44 +1000 Subject: [PATCH 446/512] Do not use transparency if it has been removed when normalizing mode --- Tests/test_file_gif.py | 15 +++++++++++++++ src/PIL/GifImagePlugin.py | 6 +----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index f4a17264f..b1c9f731f 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1086,6 +1086,21 @@ def test_transparent_optimize(tmp_path): assert reloaded.info["transparency"] == reloaded.getpixel((252, 0)) +def test_removed_transparency(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("RGB", (256, 1)) + + for x in range(256): + im.putpixel((x, 0), (x, 0, 0)) + + im.info["transparency"] = (255, 255, 255) + with pytest.warns(UserWarning): + im.save(out) + + with Image.open(out) as reloaded: + assert "transparency" not in reloaded.info + + def test_rgb_transparency(tmp_path): out = str(tmp_path / "temp.gif") diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index cf2993e38..255643de6 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -683,11 +683,7 @@ def get_interlace(im): def _write_local_header(fp, im, offset, flags): transparent_color_exists = False try: - if "transparency" in im.encoderinfo: - transparency = im.encoderinfo["transparency"] - else: - transparency = im.info["transparency"] - transparency = int(transparency) + transparency = int(im.encoderinfo["transparency"]) except (KeyError, ValueError): pass else: From 7b2c803c5646494c2824f1af50145ad5f70a8b3f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 13 Jul 2023 08:32:44 +0300 Subject: [PATCH 447/512] Fix 3.1 to 3.10 Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/newer-versions.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/newer-versions.csv b/docs/newer-versions.csv index c228b7c29..1457d59de 100644 --- a/docs/newer-versions.csv +++ b/docs/newer-versions.csv @@ -1,4 +1,4 @@ -Python,3.12,3.11,3.1,3.9,3.8,3.7,3.6,3.5 +Python,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 Pillow >= 10.1,Yes,Yes,Yes,Yes,Yes,,, Pillow 10.0,,Yes,Yes,Yes,Yes,,, Pillow 9.3 - 9.5,,Yes,Yes,Yes,Yes,Yes,, From 414694e190445dd60878b7df341a755fac51b9f8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 13 Jul 2023 17:21:06 +1000 Subject: [PATCH 448/512] Increment Python version check to support Python 3.12 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index baf7b9395..935166716 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ TIFF_ROOT = None ZLIB_ROOT = None FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ -if sys.platform == "win32" and sys.version_info >= (3, 12): +if sys.platform == "win32" and sys.version_info >= (3, 13): import atexit atexit.register( From 1f3ec1b8c9bcb5c93016f7c2d8c43912114e0580 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Jul 2023 18:25:26 +1000 Subject: [PATCH 449/512] Include NumPy version in Cygwin pip cache key --- .github/workflows/test-cygwin.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index e7ab6466e..2ba5e741e 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -76,17 +76,23 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' + - name: Select Python version + run: | + ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + + - name: Get latest NumPy version + id: latest-numpy + shell: bash.exe -eo pipefail -o igncr "{0}" + run: | + python3 -m pip list --outdated | grep numpy | sed -r 's/ +/ /g' | cut -d ' ' -f 3 | sed 's/^/version=/' >> $GITHUB_OUTPUT + - name: pip cache uses: actions/cache@v3 with: path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} + key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}-${{ hashFiles('.ci/install.sh') }} restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 + ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-numpy${{ steps.latest-numpy.outputs.version }}- - name: Build system information run: | @@ -96,7 +102,7 @@ jobs: run: | bash.exe .ci/install.sh - - name: Install a different NumPy + - name: Install latest NumPy shell: dash.exe -l "{0}" run: | python3 -m pip install -U numpy From f965b9a81f24f5eeadc7e869d68b5aa828ecb966 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 14 Jul 2023 22:45:29 +1000 Subject: [PATCH 450/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 41faa1310..c27f333ab 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 + [radarhere] + - Fix missing symbols when libtiff depends on libjpeg #7270 [heitbaum] From 7ffad80294bd4e1366f41f154120c314079e9fb8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Jul 2023 20:59:28 +1000 Subject: [PATCH 451/512] Removed put_pixel --- src/libImaging/Access.c | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index f00939da0..f7adbe198 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -127,15 +127,6 @@ get_pixel_32B(Imaging im, int x, int y, void *color) { /* store individual pixel */ -static void -put_pixel(Imaging im, int x, int y, const void *color) { - if (im->image8) { - im->image8[y][x] = *((UINT8 *)color); - } else { - memcpy(&im->image32[y][x], color, sizeof(INT32)); - } -} - static void put_pixel_8(Imaging im, int x, int y, const void *color) { im->image8[y][x] = *((UINT8 *)color); @@ -186,8 +177,8 @@ ImagingAccessInit() { /* populate access table */ ADD("1", get_pixel_8, put_pixel_8); ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel, put_pixel); - ADD("La", get_pixel, put_pixel); + ADD("LA", get_pixel, put_pixel_32); + ADD("La", get_pixel, put_pixel_32); ADD("I", get_pixel_32, put_pixel_32); ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); @@ -197,7 +188,7 @@ ImagingAccessInit() { ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel, put_pixel); + ADD("PA", get_pixel, put_pixel_32); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); From 577a4d8bf83fa390fdddf631423fbd6b57573f21 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 21 Jul 2023 21:08:20 +1000 Subject: [PATCH 452/512] Change get_pixel to be specific to images with 2 bands --- src/libImaging/Access.c | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index f7adbe198..dd0418696 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -46,22 +46,11 @@ add_item(const char *mode) { /* fetch individual pixel */ static void -get_pixel(Imaging im, int x, int y, void *color) { +get_pixel_32_2bands(Imaging im, int x, int y, void *color) { char *out = color; - - /* generic pixel access*/ - - if (im->image8) { - out[0] = im->image8[y][x]; - } else { - UINT8 *p = (UINT8 *)&im->image32[y][x]; - if (im->type == IMAGING_TYPE_UINT8 && im->bands == 2) { - out[0] = p[0]; - out[1] = p[3]; - return; - } - memcpy(out, p, im->pixelsize); - } + UINT8 *p = (UINT8 *)&im->image32[y][x]; + out[0] = p[0]; + out[1] = p[3]; } static void @@ -177,8 +166,8 @@ ImagingAccessInit() { /* populate access table */ ADD("1", get_pixel_8, put_pixel_8); ADD("L", get_pixel_8, put_pixel_8); - ADD("LA", get_pixel, put_pixel_32); - ADD("La", get_pixel, put_pixel_32); + ADD("LA", get_pixel_32_2bands, put_pixel_32); + ADD("La", get_pixel_32_2bands, put_pixel_32); ADD("I", get_pixel_32, put_pixel_32); ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); @@ -188,7 +177,7 @@ ImagingAccessInit() { ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); ADD("P", get_pixel_8, put_pixel_8); - ADD("PA", get_pixel, put_pixel_32); + ADD("PA", get_pixel_32_2bands, put_pixel_32); ADD("RGB", get_pixel_32, put_pixel_32); ADD("RGBA", get_pixel_32, put_pixel_32); ADD("RGBa", get_pixel_32, put_pixel_32); From 064cd6fb830fc50d879091761e14047d7c5dc350 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Jul 2023 18:47:55 +1000 Subject: [PATCH 453/512] Added more information about PPM formats --- 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 fbaca5ae7..bd4995fc0 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -861,6 +861,10 @@ PPM Pillow reads and writes PBM, PGM, PPM and PNM files containing ``1``, ``L``, ``I`` or ``RGB`` data. +"Raw" (P4 to P6) formats can be read, and are used when writing. + +Since Pillow 9.2.0, "plain" (P1 to P3) formats can be read as well. + SGI ^^^ From c108d9ddb033e3e70f7261b7f1b7fc9d81c00259 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Jul 2023 20:00:36 +1000 Subject: [PATCH 454/512] Set alpha channel when saving LA in OpenJPEG --- src/libImaging/Jpeg2KEncode.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libImaging/Jpeg2KEncode.c b/src/libImaging/Jpeg2KEncode.c index de8586706..3295373fd 100644 --- a/src/libImaging/Jpeg2KEncode.c +++ b/src/libImaging/Jpeg2KEncode.c @@ -490,6 +490,8 @@ j2k_encode_entry(Imaging im, ImagingCodecState state) { if (strcmp(im->mode, "RGBA") == 0) { image->comps[3].alpha = 1; + } else if (strcmp(im->mode, "LA") == 0) { + image->comps[1].alpha = 1; } opj_set_error_handler(codec, j2k_error, context); From 26ca569cbe0f82be411fbc03e674badf1540d19a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Jul 2023 20:00:42 +1000 Subject: [PATCH 455/512] Parametrized test --- Tests/test_file_jpeg2k.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index b6e8215f7..99df26fc9 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -274,17 +274,15 @@ def test_sgnd(tmp_path): assert reloaded_signed.getpixel((0, 0)) == 128 -def test_rgba(): +@pytest.mark.parametrize("ext", (".j2k", ".jp2")) +def test_rgba(ext): # 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() + with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: + # Act + im.load() - # Assert - assert j2k.mode == "RGBA" - assert jp2.mode == "RGBA" + # Assert + assert im.mode == "RGBA" @pytest.mark.parametrize("ext", (".j2k", ".jp2")) From 0a0a3fc51fef349f7314fd007745d9078dd2325a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 22 Jul 2023 20:13:55 +1000 Subject: [PATCH 456/512] Added saving LA images as PDFs --- Tests/test_file_pdf.py | 9 +++++---- docs/handbook/image-file-formats.rst | 2 +- src/PIL/PdfImagePlugin.py | 5 +++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 967f5c35e..9c8e90b7e 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -43,8 +43,9 @@ def test_save(tmp_path, mode): @skip_unless_feature("jpg_2000") -def test_save_rgba(tmp_path): - helper_save_as_pdf(tmp_path, "RGBA") +@pytest.mark.parametrize("mode", ("LA", "RGBA")) +def test_save_alpha(tmp_path, mode): + helper_save_as_pdf(tmp_path, mode) def test_monochrome(tmp_path): @@ -57,8 +58,8 @@ def test_monochrome(tmp_path): def test_unsupported_mode(tmp_path): - im = hopper("LA") - outfile = str(tmp_path / "temp_LA.pdf") + im = hopper("PA") + outfile = str(tmp_path / "temp_PA.pdf") with pytest.raises(ValueError): im.save(outfile) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index bd4995fc0..0d8d9bc10 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1486,7 +1486,7 @@ files. Different encoding methods are used, depending on the image mode. unavailable * L, RGB and CMYK mode images use JPEG encoding * P mode images use HEX encoding -* RGBA mode images use JPEG2000 encoding +* LA and RGBA mode images use JPEG2000 encoding .. _pdf-saving: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index c41f8aee0..a0fbae050 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -159,6 +159,11 @@ def _save(im, fp, filename, save_all=False): # params = f"<< /Predictor 15 /Columns {width-2} >>" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale + elif im.mode == "LA": + filter = "JPXDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + colorspace = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale elif im.mode == "P": filter = "ASCIIHexDecode" palette = im.getpalette() From e5c94eced225bc626600c9964a2af83932478baf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 23 Jul 2023 16:26:42 +1000 Subject: [PATCH 457/512] Simplified code --- src/PIL/Image.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 4b088bdfd..5a3de65cd 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -641,9 +641,8 @@ class Image: b = io.BytesIO() try: self.save(b, image_format, **kwargs) - except Exception as e: - msg = f"Could not save to {image_format} for display" - raise ValueError(msg) from e + except Exception: + return None return b.getvalue() def _repr_png_(self): @@ -651,20 +650,14 @@ class Image: :returns: PNG version of the image as bytes """ - try: - return self._repr_image("PNG", compress_level=1) - except Exception: - return None + return self._repr_image("PNG", compress_level=1) def _repr_jpeg_(self): """iPython display hook support for JPEG format. :returns: JPEG version of the image as bytes """ - try: - return self._repr_image("JPEG") - except Exception: - return None + return self._repr_image("JPEG") @property def __array_interface__(self): From 62cd236d1a4ed4afbe311ff9bc97c44bc9eb8a39 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 25 Jul 2023 12:31:58 +0300 Subject: [PATCH 458/512] Clarify that a single value is returned, and depends on the text direction --- docs/reference/ImageDraw.rst | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 31f63695e..95a40007b 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -538,7 +538,7 @@ Methods It should be a `BCP 47 language code`_. Requires libraqm. :param embedded_color: Whether to use font embedded color glyphs (COLR, CBDT, SBIX). - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. .. py:method:: ImageDraw.textbbox(xy, text, font=None, anchor=None, spacing=4, align="left", direction=None, features=None, language=None, stroke_width=0, embedded_color=False) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 05828a72f..b77dc6ab1 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -318,7 +318,7 @@ class FreeTypeFont: `_ Requires libraqm. - :return: Width for horizontal, height for vertical text. + :return: Either width for horizontal text, or height for vertical text. """ _string_length_check(text) return self.font.getlength(text, mode, direction, features, language) / 64 From 9979a822c731833fcf52e12a20e9109f84dc1aae Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 29 Jul 2023 09:28:18 +1000 Subject: [PATCH 459/512] Changed Image mode property to be read-only by default --- Tests/test_image.py | 6 +++++ Tests/test_imagefile.py | 4 ++-- Tests/test_pickle.py | 4 ++-- docs/example/DdsImagePlugin.py | 2 +- .../writing-your-own-image-plugin.rst | 6 ++--- src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 10 ++++----- src/PIL/BufrStubImagePlugin.py | 2 +- src/PIL/DdsImagePlugin.py | 22 +++++++++---------- src/PIL/EpsImagePlugin.py | 8 +++---- src/PIL/FitsImagePlugin.py | 8 +++---- src/PIL/FliImagePlugin.py | 2 +- src/PIL/FpxImagePlugin.py | 2 +- src/PIL/FtexImagePlugin.py | 4 ++-- src/PIL/GbrImagePlugin.py | 4 ++-- src/PIL/GdImageFile.py | 2 +- src/PIL/GifImagePlugin.py | 18 +++++++-------- src/PIL/GribStubImagePlugin.py | 2 +- src/PIL/Hdf5StubImagePlugin.py | 2 +- src/PIL/IcnsImagePlugin.py | 4 ++-- src/PIL/IcoImagePlugin.py | 2 +- src/PIL/ImImagePlugin.py | 6 ++--- src/PIL/Image.py | 18 +++++++++------ src/PIL/ImtImagePlugin.py | 2 +- src/PIL/IptcImagePlugin.py | 6 ++--- src/PIL/Jpeg2KImagePlugin.py | 4 ++-- src/PIL/JpegImagePlugin.py | 10 ++++----- src/PIL/McIdasImagePlugin.py | 2 +- src/PIL/MpegImagePlugin.py | 2 +- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PcdImagePlugin.py | 2 +- src/PIL/PcxImagePlugin.py | 2 +- src/PIL/PixarImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 2 +- src/PIL/PpmImagePlugin.py | 6 ++--- src/PIL/PsdImagePlugin.py | 4 ++-- src/PIL/QoiImagePlugin.py | 2 +- src/PIL/SgiImagePlugin.py | 2 +- src/PIL/SpiderImagePlugin.py | 2 +- src/PIL/SunImagePlugin.py | 16 +++++++------- src/PIL/TgaImagePlugin.py | 12 +++++----- src/PIL/TiffImagePlugin.py | 4 ++-- src/PIL/WalImageFile.py | 2 +- src/PIL/WebPImagePlugin.py | 4 ++-- src/PIL/WmfImagePlugin.py | 4 ++-- src/PIL/XVThumbImagePlugin.py | 2 +- src/PIL/XbmImagePlugin.py | 2 +- src/PIL/XpmImagePlugin.py | 2 +- 48 files changed, 125 insertions(+), 115 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 85f9f7d02..36f24379a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -135,6 +135,12 @@ class TestImage: with pytest.raises(AttributeError): im.size = (3, 4) + def test_set_mode(self): + im = Image.new("RGB", (1, 1)) + + with pytest.raises(AttributeError): + im.mode = "P" + def test_invalid_image(self): im = io.BytesIO(b"") with pytest.raises(UnidentifiedImageError): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 412bc10d9..ff75b8c2a 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -136,7 +136,7 @@ class TestImageFile: class DummyImageFile(ImageFile.ImageFile): def _open(self): - self.mode = "RGB" + self._mode = "RGB" self._size = (1, 1) im = DummyImageFile(buf) @@ -217,7 +217,7 @@ xoff, yoff, xsize, ysize = 10, 20, 100, 100 class MockImageFile(ImageFile.ImageFile): def _open(self): self.rawmode = "RGBA" - self.mode = "RGBA" + self._mode = "RGBA" self._size = (200, 200) self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 2f6d05888..531936f8f 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -75,13 +75,13 @@ def test_pickle_la_mode_with_palette(tmp_path): # Act / Assert for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im.mode = "LA" + im._mode = "LA" with open(filename, "wb") as f: pickle.dump(im, f, protocol) with open(filename, "rb") as f: loaded_im = pickle.load(f) - im.mode = "PA" + im._mode = "PA" assert im == loaded_im diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 26451533e..61690410b 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -225,7 +225,7 @@ class DdsImageFile(ImageFile.ImageFile): flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) - self.mode = "RGBA" + self._mode = "RGBA" pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 75604e17a..ca16fccda 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -72,11 +72,11 @@ true color. # mode setting bits = int(header[3]) if bits == 1: - self.mode = "1" + self._mode = "1" elif bits == 8: - self.mode = "L" + self._mode = "L" elif bits == 24: - self.mode = "RGB" + self._mode = "RGB" else: msg = "unknown number of bits" raise SyntaxError(msg) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 0ca60ff24..a5cfad5f4 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -266,7 +266,7 @@ class BlpImageFile(ImageFile.ImageFile): msg = f"Bad BLP magic {repr(self.magic)}" raise BLPFormatError(msg) - self.mode = "RGBA" if self._blp_alpha_depth else "RGB" + self._mode = "RGBA" if self._blp_alpha_depth else "RGB" self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index 5bda0a5b0..9abfd0b5b 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -163,7 +163,7 @@ class BmpImageFile(ImageFile.ImageFile): offset += 4 * file_info["colors"] # ---------------------- Check bit depth for unusual unsupported values - self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) + self._mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None)) if self.mode is None: msg = f"Unsupported BMP pixel depth ({file_info['bits']})" raise OSError(msg) @@ -200,7 +200,7 @@ class BmpImageFile(ImageFile.ImageFile): and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]] ): raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])] - self.mode = "RGBA" if "A" in raw_mode else self.mode + self._mode = "RGBA" if "A" in raw_mode else self.mode elif ( file_info["bits"] in (24, 16) and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]] @@ -214,7 +214,7 @@ class BmpImageFile(ImageFile.ImageFile): raise OSError(msg) elif file_info["compression"] == self.RAW: if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset - raw_mode, self.mode = "BGRA", "RGBA" + raw_mode, self._mode = "BGRA", "RGBA" elif file_info["compression"] in (self.RLE8, self.RLE4): decoder_name = "bmp_rle" else: @@ -245,10 +245,10 @@ class BmpImageFile(ImageFile.ImageFile): # ------- If all colors are grey, white or black, ditch palette if greyscale: - self.mode = "1" if file_info["colors"] == 2 else "L" + self._mode = "1" if file_info["colors"] == 2 else "L" raw_mode = self.mode else: - self.mode = "P" + self._mode = "P" self.palette = ImagePalette.raw( "BGRX" if padding == 4 else "BGR", palette ) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0425bbd75..eef25aa14 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -46,7 +46,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a946daeaa..1368ae24e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -128,7 +128,7 @@ class DdsImageFile(ImageFile.ImageFile): flags, height, width = struct.unpack("<3I", header.read(12)) self._size = (width, height) - self.mode = "RGBA" + self._mode = "RGBA" pitch, depth, mipmaps = struct.unpack("<3I", header.read(12)) struct.unpack("<11I", header.read(44)) # reserved @@ -141,9 +141,9 @@ class DdsImageFile(ImageFile.ImageFile): if pfflags & DDPF_LUMINANCE: # Texture contains uncompressed L or LA data if pfflags & DDPF_ALPHAPIXELS: - self.mode = "LA" + self._mode = "LA" else: - self.mode = "L" + self._mode = "L" self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))] elif pfflags & DDPF_RGB: @@ -153,7 +153,7 @@ class DdsImageFile(ImageFile.ImageFile): if pfflags & DDPF_ALPHAPIXELS: rawmode += masks[0xFF000000] else: - self.mode = "RGB" + self._mode = "RGB" rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF] self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))] @@ -172,15 +172,15 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"ATI1": self.pixel_format = "BC4" n = 4 - self.mode = "L" + self._mode = "L" elif fourcc == b"ATI2": self.pixel_format = "BC5" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif fourcc == b"BC5S": self.pixel_format = "BC5S" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif fourcc == b"DX10": data_start += 20 # ignoring flags which pertain to volume textures and cubemaps @@ -189,19 +189,19 @@ class DdsImageFile(ImageFile.ImageFile): if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM): self.pixel_format = "BC5" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC5_SNORM: self.pixel_format = "BC5S" n = 5 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC6H_UF16: self.pixel_format = "BC6H" n = 6 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format == DXGI_FORMAT_BC6H_SF16: self.pixel_format = "BC6HS" n = 6 - self.mode = "RGB" + self._mode = "RGB" elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM): self.pixel_format = "BC7" n = 7 diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 6b1b5947e..b96ce9603 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -227,7 +227,7 @@ class EpsImageFile(ImageFile.ImageFile): # go to offset - start of "%!PS" self.fp.seek(offset) - self.mode = "RGB" + self._mode = "RGB" self._size = None byte_arr = bytearray(255) @@ -344,10 +344,10 @@ class EpsImageFile(ImageFile.ImageFile): ] if bit_depth == 1: - self.mode = "1" + self._mode = "1" elif bit_depth == 8: try: - self.mode = self.mode_map[mode_id] + self._mode = self.mode_map[mode_id] except ValueError: break else: @@ -391,7 +391,7 @@ class EpsImageFile(ImageFile.ImageFile): # Load EPS via Ghostscript if self.tile: self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) - self.mode = self.im.mode + self._mode = self.im.mode self._size = self.im.size self.tile = [] return Image.Image.load(self) diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index 1359aeb12..e0e51aaac 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -51,14 +51,14 @@ class FitsImageFile(ImageFile.ImageFile): number_of_bits = int(headers[b"BITPIX"]) if number_of_bits == 8: - self.mode = "L" + self._mode = "L" elif number_of_bits == 16: - self.mode = "I" + self._mode = "I" # rawmode = "I;16S" elif number_of_bits == 32: - self.mode = "I" + self._mode = "I" elif number_of_bits in (-32, -64): - self.mode = "F" + self._mode = "F" # rawmode = "F" if number_of_bits == -32 else "F;64F" offset = math.ceil(self.fp.tell() / 2880) * 2880 diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index f4e89a03e..8f641ece9 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -56,7 +56,7 @@ class FliImageFile(ImageFile.ImageFile): self.is_animated = self.n_frames > 1 # image characteristics - self.mode = "P" + self._mode = "P" self._size = i16(s, 8), i16(s, 10) # animation speed diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 2450c67e9..a878cbfd2 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -106,7 +106,7 @@ class FpxImageFile(ImageFile.ImageFile): # note: for now, we ignore the "uncalibrated" flag colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF) - self.mode, self.rawmode = MODES[tuple(colors)] + self._mode, self.rawmode = MODES[tuple(colors)] # load JPEG tables, if any self.jpeg = {} diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index c46b2f28b..c2e4ead71 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -77,7 +77,7 @@ class FtexImageFile(ImageFile.ImageFile): self._size = struct.unpack("<2i", self.fp.read(8)) mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8)) - self.mode = "RGB" + self._mode = "RGB" # Only support single-format files. # I don't know of any multi-format file. @@ -90,7 +90,7 @@ class FtexImageFile(ImageFile.ImageFile): data = self.fp.read(mipmap_size) if format == Format.DXT1: - self.mode = "RGBA" + self._mode = "RGBA" self.tile = [("bcn", (0, 0) + self.size, 0, 1)] elif format == Format.UNCOMPRESSED: self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))] diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index 994a6e8eb..ec6e9de6e 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -73,9 +73,9 @@ class GbrImageFile(ImageFile.ImageFile): comment = self.fp.read(comment_length)[:-1] if color_depth == 1: - self.mode = "L" + self._mode = "L" else: - self.mode = "RGBA" + self._mode = "RGBA" self._size = width, height diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index bafc43a19..3599994a8 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -51,7 +51,7 @@ class GdImageFile(ImageFile.ImageFile): msg = "Not a valid GD 2.x .gd file" raise SyntaxError(msg) - self.mode = "L" # FIXME: "P" + self._mode = "L" # FIXME: "P" self._size = i16(s, 2), i16(s, 4) true_color = s[6] diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 255643de6..943842f77 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -304,11 +304,11 @@ class GifImageFile(ImageFile.ImageFile): if frame == 0: if self._frame_palette: if LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: - self.mode = "RGBA" if frame_transparency is not None else "RGB" + self._mode = "RGBA" if frame_transparency is not None else "RGB" else: - self.mode = "P" + self._mode = "P" else: - self.mode = "L" + self._mode = "L" if not palette and self.global_palette: from copy import copy @@ -325,10 +325,10 @@ class GifImageFile(ImageFile.ImageFile): if "transparency" in self.info: self.im.putpalettealpha(self.info["transparency"], 0) self.im = self.im.convert("RGBA", Image.Dither.FLOYDSTEINBERG) - self.mode = "RGBA" + self._mode = "RGBA" del self.info["transparency"] else: - self.mode = "RGB" + self._mode = "RGB" self.im = self.im.convert("RGB", Image.Dither.FLOYDSTEINBERG) def _rgb(color): @@ -424,7 +424,7 @@ class GifImageFile(ImageFile.ImageFile): self.im.putpalette(*self._frame_palette.getdata()) else: self.im = None - self.mode = temp_mode + self._mode = temp_mode self._frame_palette = None super().load_prepare() @@ -434,9 +434,9 @@ class GifImageFile(ImageFile.ImageFile): if self.mode == "P" and LOADING_STRATEGY == LoadingStrategy.RGB_ALWAYS: if self._frame_transparency is not None: self.im.putpalettealpha(self._frame_transparency, 0) - self.mode = "RGBA" + self._mode = "RGBA" else: - self.mode = "RGB" + self._mode = "RGB" self.im = self.im.convert(self.mode, Image.Dither.FLOYDSTEINBERG) return if not self._prev_im: @@ -449,7 +449,7 @@ class GifImageFile(ImageFile.ImageFile): frame_im = self._crop(frame_im, self.dispose_extent) self.im = self._prev_im - self.mode = self.im.mode + self._mode = self.im.mode if frame_im.mode == "RGBA": self.im.paste(frame_im, self.dispose_extent, frame_im) else: diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 8a799f19c..c1c71da08 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -46,7 +46,7 @@ class GribStubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index bba05ed65..c26b480ac 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -46,7 +46,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): self.fp.seek(offset) # make something up - self.mode = "F" + self._mode = "F" self._size = 1, 1 loader = self._load() diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index 27cb89f73..0aa4f7a84 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -253,7 +253,7 @@ class IcnsImageFile(ImageFile.ImageFile): def _open(self): self.icns = IcnsFile(self.fp) - self.mode = "RGBA" + self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() self.best_size = self.icns.bestsize() self.size = ( @@ -305,7 +305,7 @@ class IcnsImageFile(ImageFile.ImageFile): px = im.load() self.im = im.im - self.mode = im.mode + self._mode = im.mode self.size = im.size return px diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index a188f8fdc..0445a2ab2 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -330,7 +330,7 @@ class IcoImageFile(ImageFile.ImageFile): im.load() self.im = im.im self.pyaccess = None - self.mode = im.mode + self._mode = im.mode if im.size != self.size: warnings.warn("Image was not the expected size") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 746743f65..b42ba7cac 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -205,7 +205,7 @@ class ImImageFile(ImageFile.ImageFile): # Basic attributes self._size = self.info[SIZE] - self.mode = self.info[MODE] + self._mode = self.info[MODE] # Skip forward to start of image data while s and s[:1] != b"\x1A": @@ -231,9 +231,9 @@ class ImImageFile(ImageFile.ImageFile): self.lut = list(palette[:256]) else: if self.mode in ["L", "P"]: - self.mode = self.rawmode = "P" + self._mode = self.rawmode = "P" elif self.mode in ["LA", "PA"]: - self.mode = "PA" + self._mode = "PA" self.rawmode = "PA;L" self.palette = ImagePalette.raw("RGB;L", palette) elif self.mode == "RGB": diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a519a28af..6a9ac5a2a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -482,7 +482,7 @@ class Image: # FIXME: take "new" parameters / other image? # FIXME: turn mode and size into delegating properties? self.im = None - self.mode = "" + self._mode = "" self._size = (0, 0) self.palette = None self.info = {} @@ -502,10 +502,14 @@ class Image: def size(self): return self._size + @property + def mode(self): + return self._mode + def _new(self, im): new = Image() new.im = im - new.mode = im.mode + new._mode = im.mode new._size = im.size if im.mode in ("P", "PA"): if self.palette: @@ -693,7 +697,7 @@ class Image: Image.__init__(self) info, mode, size, palette, data = state self.info = info - self.mode = mode + self._mode = mode self._size = size self.im = core.new(mode, size) if mode in ("L", "LA", "P", "PA") and palette: @@ -1840,7 +1844,7 @@ class Image: raise ValueError from e # sanity check self.im = im self.pyaccess = None - self.mode = self.im.mode + self._mode = self.im.mode except KeyError as e: msg = "illegal image mode" raise ValueError(msg) from e @@ -1918,7 +1922,7 @@ class Image: if not isinstance(data, bytes): data = bytes(data) palette = ImagePalette.raw(rawmode, data) - self.mode = "PA" if "A" in self.mode else "P" + self._mode = "PA" if "A" in self.mode else "P" self.palette = palette self.palette.mode = "RGB" self.load() # install new palette @@ -2026,7 +2030,7 @@ class Image: mapping_palette = bytearray(new_positions) m_im = self.copy() - m_im.mode = "P" + m_im._mode = "P" m_im.palette = ImagePalette.ImagePalette( palette_mode, palette=mapping_palette * bands @@ -2601,7 +2605,7 @@ class Image: self.im = im.im self._size = size - self.mode = self.im.mode + self._mode = self.im.mode self.readonly = 0 self.pyaccess = None diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py index ac267457b..d409fcd59 100644 --- a/src/PIL/ImtImagePlugin.py +++ b/src/PIL/ImtImagePlugin.py @@ -89,7 +89,7 @@ class ImtImageFile(ImageFile.ImageFile): ysize = int(v) self._size = xsize, ysize elif k == b"pixel" and v == b"n8": - self.mode = "L" + self._mode = "L" # diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 4c47b55c1..6ce4975c0 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -109,11 +109,11 @@ class IptcImageFile(ImageFile.ImageFile): else: id = 0 if layers == 1 and not component: - self.mode = "L" + self._mode = "L" elif layers == 3 and component: - self.mode = "RGB"[id] + self._mode = "RGB"[id] elif layers == 4 and component: - self.mode = "CMYK"[id] + self._mode = "CMYK"[id] # size self._size = self.getint((3, 20)), self.getint((3, 30)) diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 9309768ba..963d6c1a3 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -208,14 +208,14 @@ class Jpeg2KImageFile(ImageFile.ImageFile): sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" - self._size, self.mode = _parse_codestream(self.fp) + self._size, self._mode = _parse_codestream(self.fp) else: sig = sig + self.fp.read(8) if sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a": self.codec = "jp2" header = _parse_jp2_header(self.fp) - self._size, self.mode, self.custom_mimetype, dpi = header + 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"): diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index dfc7e6e9f..475e54c2b 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -208,11 +208,11 @@ def SOF(self, marker): self.layers = s[5] if self.layers == 1: - self.mode = "L" + self._mode = "L" elif self.layers == 3: - self.mode = "RGB" + self._mode = "RGB" elif self.layers == 4: - self.mode = "CMYK" + self._mode = "CMYK" else: msg = f"cannot handle {self.layers}-layer images" raise SyntaxError(msg) @@ -426,7 +426,7 @@ class JpegImageFile(ImageFile.ImageFile): original_size = self.size if a[0] == "RGB" and mode in ["L", "YCbCr"]: - self.mode = mode + self._mode = mode a = mode, "" if size: @@ -475,7 +475,7 @@ class JpegImageFile(ImageFile.ImageFile): except OSError: pass - self.mode = self.im.mode + self._mode = self.im.mode self._size = self.im.size self.tile = [] diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py index 17c008b9a..bb79e71de 100644 --- a/src/PIL/McIdasImagePlugin.py +++ b/src/PIL/McIdasImagePlugin.py @@ -58,7 +58,7 @@ class McIdasImageFile(ImageFile.ImageFile): msg = "unsupported McIdas format" raise SyntaxError(msg) - self.mode = mode + self._mode = mode self._size = w[10], w[9] offset = w[34] + w[15] diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index d96d3a11c..bfa88fe99 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -68,7 +68,7 @@ class MpegImageFile(ImageFile.ImageFile): msg = "not an MPEG file" raise SyntaxError(msg) - self.mode = "RGB" + self._mode = "RGB" self._size = s.read(12), s.read(12) diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index c6567b2ae..3f3609f1c 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -62,7 +62,7 @@ class MspImageFile(ImageFile.ImageFile): msg = "bad MSP checksum" raise SyntaxError(msg) - self.mode = "1" + self._mode = "1" self._size = i16(s, 4), i16(s, 6) if s[:4] == b"DanM": diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py index e390f3fe5..c7cbca8c5 100644 --- a/src/PIL/PcdImagePlugin.py +++ b/src/PIL/PcdImagePlugin.py @@ -43,7 +43,7 @@ class PcdImageFile(ImageFile.ImageFile): elif orientation == 3: self.tile_post_rotate = -90 - self.mode = "RGB" + self._mode = "RGB" self._size = 768, 512 # FIXME: not correct for rotated images! self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)] diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index f42c2456b..854d9e83e 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -108,7 +108,7 @@ class PcxImageFile(ImageFile.ImageFile): msg = "unknown PCX mode" raise OSError(msg) - self.mode = mode + self._mode = mode self._size = bbox[2] - bbox[0], bbox[3] - bbox[1] # Don't trust the passed in stride. diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py index 7eb82228a..850272311 100644 --- a/src/PIL/PixarImagePlugin.py +++ b/src/PIL/PixarImagePlugin.py @@ -54,7 +54,7 @@ class PixarImageFile(ImageFile.ImageFile): mode = i16(s, 424), i16(s, 426) if mode == (14, 2): - self.mode = "RGB" + self._mode = "RGB" # FIXME: to be continued... # create tile descriptor (assuming "dumped") diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index bfa8cb7ac..2ed182d32 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -737,7 +737,7 @@ class PngImageFile(ImageFile.ImageFile): # difficult to break if things go wrong in the decoder... # (believe me, I've tried ;-) - self.mode = self.png.im_mode + self._mode = self.png.im_mode self._size = self.png.im_size self.info = self.png.im_info self._text = None diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 2cb1e5636..e480ab055 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -116,18 +116,18 @@ class PpmImageFile(ImageFile.ImageFile): elif ix == 1: # token is the y size ysize = token if mode == "1": - self.mode = "1" + self._mode = "1" rawmode = "1;I" break else: - self.mode = rawmode = mode + self._mode = rawmode = mode elif ix == 2: # token is maxval maxval = token if not 0 < maxval < 65536: msg = "maxval must be greater than 0 and less than 65536" raise ValueError(msg) if maxval > 255 and mode == "L": - self.mode = "I" + self._mode = "I" if decoder_name != "ppm_plain": # If maxval matches a bit depth, use the raw decoder directly diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 5a5d60d56..2f019bb8c 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -79,7 +79,7 @@ class PsdImageFile(ImageFile.ImageFile): mode = "RGBA" channels = 4 - self.mode = mode + self._mode = mode self._size = i32(s, 18), i32(s, 14) # @@ -146,7 +146,7 @@ class PsdImageFile(ImageFile.ImageFile): # seek to given layer (1..max) try: name, mode, bbox, tile = self.layers[layer - 1] - self.mode = mode + self._mode = mode self.tile = tile self.frame = layer self.fp = self._fp diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index ef91b90ab..5c3407503 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -29,7 +29,7 @@ class QoiImageFile(ImageFile.ImageFile): 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._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)] diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 3662ffd15..acb9ce5a3 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -94,7 +94,7 @@ class SgiImageFile(ImageFile.ImageFile): raise ValueError(msg) self._size = xsize, ysize - self.mode = rawmode.split(";")[0] + self._mode = rawmode.split(";")[0] if self.mode == "RGB": self.custom_mimetype = "image/rgb" diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 5614957c1..408b982b5 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -149,7 +149,7 @@ class SpiderImageFile(ImageFile.ImageFile): self.rawmode = "F;32BF" else: self.rawmode = "F;32F" - self.mode = "F" + self._mode = "F" self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))] self._fp = self.fp # FIXME: hack diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py index 6712583d7..6a8d5d86b 100644 --- a/src/PIL/SunImagePlugin.py +++ b/src/PIL/SunImagePlugin.py @@ -66,21 +66,21 @@ class SunImageFile(ImageFile.ImageFile): palette_length = i32(s, 28) if depth == 1: - self.mode, rawmode = "1", "1;I" + self._mode, rawmode = "1", "1;I" elif depth == 4: - self.mode, rawmode = "L", "L;4" + self._mode, rawmode = "L", "L;4" elif depth == 8: - self.mode = rawmode = "L" + self._mode = rawmode = "L" elif depth == 24: if file_type == 3: - self.mode, rawmode = "RGB", "RGB" + self._mode, rawmode = "RGB", "RGB" else: - self.mode, rawmode = "RGB", "BGR" + self._mode, rawmode = "RGB", "BGR" elif depth == 32: if file_type == 3: - self.mode, rawmode = "RGB", "RGBX" + self._mode, rawmode = "RGB", "RGBX" else: - self.mode, rawmode = "RGB", "BGRX" + self._mode, rawmode = "RGB", "BGRX" else: msg = "Unsupported Mode/Bit Depth" raise SyntaxError(msg) @@ -97,7 +97,7 @@ class SunImageFile(ImageFile.ImageFile): offset = offset + palette_length self.palette = ImagePalette.raw("RGB;L", self.fp.read(palette_length)) if self.mode == "L": - self.mode = "P" + self._mode = "P" rawmode = rawmode.replace("L", "P") # 16 bit boundaries on stride diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 67dfc3d3c..f24ee4f5c 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -76,17 +76,17 @@ class TgaImageFile(ImageFile.ImageFile): # image mode if imagetype in (3, 11): - self.mode = "L" + self._mode = "L" if depth == 1: - self.mode = "1" # ??? + self._mode = "1" # ??? elif depth == 16: - self.mode = "LA" + self._mode = "LA" elif imagetype in (1, 9): - self.mode = "P" + self._mode = "P" elif imagetype in (2, 10): - self.mode = "RGB" + self._mode = "RGB" if depth == 32: - self.mode = "RGBA" + self._mode = "RGBA" else: msg = "unknown TGA mode" raise SyntaxError(msg) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d51488285..99c23ad4b 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1409,7 +1409,7 @@ class TiffImageFile(ImageFile.ImageFile): ) logger.debug(f"format key: {key}") try: - self.mode, rawmode = OPEN_INFO[key] + self._mode, rawmode = OPEN_INFO[key] except KeyError as e: logger.debug("- unsupported format") msg = "unknown pixel mode" @@ -1461,7 +1461,7 @@ class TiffImageFile(ImageFile.ImageFile): # this should always work, since all the # fillorder==2 modes have a corresponding # fillorder=1 mode - self.mode, rawmode = OPEN_INFO[key] + self._mode, rawmode = OPEN_INFO[key] # libtiff always returns the bytes in native order. # we're expecting image byte order. So, if the rawmode # contains I;16, we need to convert from native to image diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index e4f47aa04..3d9f97f84 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -32,7 +32,7 @@ class WalImageFile(ImageFile.ImageFile): format_description = "Quake2 Texture" def _open(self): - self.mode = "P" + self._mode = "P" # read header fields header = self.fp.read(32 + 24 + 32 + 12) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index ce8e05fcb..028e5d2bd 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -43,7 +43,7 @@ class WebPImageFile(ImageFile.ImageFile): def _open(self): if not _webp.HAVE_WEBPANIM: # Legacy mode - data, width, height, self.mode, icc_profile, exif = _webp.WebPDecode( + data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( self.fp.read() ) if icc_profile: @@ -74,7 +74,7 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 - self.mode = "RGB" if mode == "RGBX" else mode + self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 0ecab56a8..3e5fb0151 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -42,7 +42,7 @@ if hasattr(Image.core, "drawwmf"): class WmfHandler: def open(self, im): - im.mode = "RGB" + im._mode = "RGB" self.bbox = im.info["wmf_bbox"] def load(self, im): @@ -139,7 +139,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): msg = "Unsupported file format" raise SyntaxError(msg) - self.mode = "RGB" + self._mode = "RGB" self._size = size loader = self._load() diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index aa4a01f4e..eda60c5c5 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -65,7 +65,7 @@ class XVThumbImageFile(ImageFile.ImageFile): # parse header line (already read) s = s.strip().split() - self.mode = "P" + self._mode = "P" self._size = int(s[0]), int(s[1]) self.palette = ImagePalette.raw("RGB", PALETTE) diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 3c12564c9..71cd57d74 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -60,7 +60,7 @@ class XbmImageFile(ImageFile.ImageFile): if m.group("hotspot"): self.info["hotspot"] = (int(m.group("xhot")), int(m.group("yhot"))) - self.mode = "1" + self._mode = "1" self._size = xsize, ysize self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 5d5bdc3ed..8491d3b7e 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -98,7 +98,7 @@ class XpmImageFile(ImageFile.ImageFile): msg = "cannot read this XPM file" raise ValueError(msg) - self.mode = "P" + self._mode = "P" self.palette = ImagePalette.raw("RGB", b"".join(palette)) self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))] From 1aa0ade1b67dee3d8cfc5359c4b1e398da517c02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Jul 2023 11:45:00 +1000 Subject: [PATCH 460/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c27f333ab..961ce563c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 + [mtreinish, radarhere] + - Do not use transparency when saving GIF if it has been removed when normalizing mode #7284 [radarhere] From 8eee4577255123ed9f7d2d984a4fa32af181cd62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 31 Jul 2023 21:06:16 +1000 Subject: [PATCH 461/512] Added release notes --- docs/releasenotes/10.1.0.rst | 54 ++++++++++++++++++++++++++++++++++++ docs/releasenotes/index.rst | 1 + 2 files changed, 55 insertions(+) create mode 100644 docs/releasenotes/10.1.0.rst diff --git a/docs/releasenotes/10.1.0.rst b/docs/releasenotes/10.1.0.rst new file mode 100644 index 000000000..da5153cce --- /dev/null +++ b/docs/releasenotes/10.1.0.rst @@ -0,0 +1,54 @@ +10.1.0 +------ + +Backwards Incompatible Changes +============================== + +Setting image mode +^^^^^^^^^^^^^^^^^^ + +If you attempt to set the mode of an image directly, e.g. +``im.mode = "RGBA"``, you will now receive an ``AttributeError``. This is +not about removing existing functionality, but instead about raising an +explicit error to prevent later consequences. The ``convert`` method is the +correct way to change an image's mode. + +Deprecations +============ + +TODO +^^^^ + +TODO + +API Changes +=========== + +TODO +^^^^ + +TODO + +API Additions +============= + +TODO +^^^^ + +TODO + +Security +======== + +TODO +^^^^ + +TODO + +Other Changes +============= + +TODO +^^^^ + +TODO diff --git a/docs/releasenotes/index.rst b/docs/releasenotes/index.rst index 9bca98541..a843ddd72 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 + 10.1.0 10.0.0 9.5.0 9.4.0 From 27a0339d647a1368424aebf092219141bf478ee4 Mon Sep 17 00:00:00 2001 From: k128 Date: Mon, 31 Jul 2023 15:14:22 -0400 Subject: [PATCH 462/512] Update WebPImagePlugin.py Automatically load duration --- src/PIL/WebPImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 028e5d2bd..f6b6332a8 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -74,6 +74,9 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 + _,ts=self._decoder.get_next() + if ts: + self.info["duration"]=ts self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] From 2f5493a5f0a7bdecf728b32839a5ce6809c3052c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:19:54 +0000 Subject: [PATCH 463/512] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/WebPImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index f6b6332a8..fc3515003 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -74,9 +74,9 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 - _,ts=self._decoder.get_next() + _, ts = self._decoder.get_next() if ts: - self.info["duration"]=ts + self.info["duration"] = ts self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] From 15e52290303008c0888dc17aab069edb5cff3a25 Mon Sep 17 00:00:00 2001 From: k128 Date: Mon, 31 Jul 2023 15:32:05 -0400 Subject: [PATCH 464/512] Update WebPImagePlugin.py --- src/PIL/WebPImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index fc3515003..47227651b 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -93,7 +93,7 @@ class WebPImageFile(ImageFile.ImageFile): self.info["xmp"] = xmp # Initialize seek state - self._reset(reset=False) + self._reset(reset=True) def _getexif(self): if "exif" not in self.info: From 6d3630d4061b32ac2f1b34cd5c852da72e3e8655 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Aug 2023 08:31:16 +1000 Subject: [PATCH 465/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 961ce563c..f19bc346e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Changed Image mode property to be read-only by default #7307 + [radarhere] + - Silence exceptions in _repr_jpeg_ and _repr_png_ #7266 [mtreinish, radarhere] From 230a2e3a339e9f6f73c08ea4e16429aa68eef723 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Aug 2023 19:15:15 +1000 Subject: [PATCH 466/512] If "reset" is always true, then the argument can be removed --- src/PIL/WebPImagePlugin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index 47227651b..aadc71f62 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -93,7 +93,7 @@ class WebPImageFile(ImageFile.ImageFile): self.info["xmp"] = xmp # Initialize seek state - self._reset(reset=True) + self._reset() def _getexif(self): if "exif" not in self.info: @@ -116,9 +116,8 @@ class WebPImageFile(ImageFile.ImageFile): # Set logical frame to requested position self.__logical_frame = frame - def _reset(self, reset=True): - if reset: - self._decoder.reset() + def _reset(self): + self._decoder.reset() self.__physical_frame = 0 self.__loaded = -1 self.__timestamp = 0 From 6115d5957f3a53a33675fdede51469cc02533649 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Aug 2023 19:16:28 +1000 Subject: [PATCH 467/512] _decoder.get_next() may return None --- src/PIL/WebPImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index aadc71f62..a6e1a2a00 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -74,9 +74,9 @@ class WebPImageFile(ImageFile.ImageFile): self.info["background"] = (bg_r, bg_g, bg_b, bg_a) self.n_frames = frame_count self.is_animated = self.n_frames > 1 - _, ts = self._decoder.get_next() - if ts: - self.info["duration"] = ts + ret = self._decoder.get_next() + if ret is not None: + self.info["duration"] = ret[1] self._mode = "RGB" if mode == "RGBX" else mode self.rawmode = mode self.tile = [] From 60433d5f37e93e462584b0e5997dad9b43cd853a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Aug 2023 19:17:54 +1000 Subject: [PATCH 468/512] Added test --- Tests/test_file_webp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a7b6c735a..3832441c0 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -233,5 +233,4 @@ class TestFileWebp: im.save(out_webp, save_all=True) with Image.open(out_webp) as reloaded: - reloaded.load() assert reloaded.info["duration"] == 1000 From c54f57c93fecc9ae608f97fa85195fec4746c00b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Aug 2023 08:57:55 +1000 Subject: [PATCH 469/512] Updated harfbuzz to 8.1.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 df834a387..9f7fd4f69 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -335,9 +335,9 @@ deps = { "libs": [r"imagequant.lib"], }, "harfbuzz": { - "url": "https://github.com/harfbuzz/harfbuzz/archive/8.0.0.zip", - "filename": "harfbuzz-8.0.0.zip", - "dir": "harfbuzz-8.0.0", + "url": "https://github.com/harfbuzz/harfbuzz/archive/8.1.1.zip", + "filename": "harfbuzz-8.1.1.zip", + "dir": "harfbuzz-8.1.1", "license": "COPYING", "build": [ *cmds_cmake( From 57f45be7652930bab754206babf9c041030df60d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Aug 2023 09:04:18 +1000 Subject: [PATCH 470/512] Updated xz to 5.4.4 --- 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 df834a387..f24a013a4 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -144,9 +144,9 @@ deps = { "libs": [r"*.lib"], }, "xz": { - "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.3.tar.gz/download", - "filename": "xz-5.4.3.tar.gz", - "dir": "xz-5.4.3", + "url": SF_PROJECTS + "/lzmautils/files/xz-5.4.4.tar.gz/download", + "filename": "xz-5.4.4.tar.gz", + "dir": "xz-5.4.4", "license": "COPYING", "build": [ *cmds_cmake("liblzma", "-DBUILD_SHARED_LIBS:BOOL=OFF"), From 022e650d18e41867f481d23197e6fd0ec35dbdd2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Aug 2023 19:11:35 +1000 Subject: [PATCH 471/512] Set SMaskInData to 1 for PDFs with alpha --- src/PIL/PdfImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index c41f8aee0..40ddf808e 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -128,6 +128,7 @@ def _save(im, fp, filename, save_all=False): bits = 8 params = None decode = None + smaskindata = 0 # # Get image characteristics @@ -177,6 +178,7 @@ def _save(im, fp, filename, save_all=False): filter = "JPXDecode" colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images + smaskindata = 1 elif im.mode == "CMYK": filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") @@ -232,6 +234,7 @@ def _save(im, fp, filename, save_all=False): Decode=decode, DecodeParms=params, ColorSpace=colorspace, + SMaskInData=smaskindata, ) # From ddfb7ef14b0a13b05f57203cacadfd295b59e48f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Aug 2023 22:05:11 +1000 Subject: [PATCH 472/512] Do not set BitsPerComponent for JPXDecode since it is ignored --- src/PIL/PdfImagePlugin.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 40ddf808e..bcb0b3937 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -125,20 +125,19 @@ def _save(im, fp, filename, save_all=False): # (packbits) or LZWDecode (tiff/lzw compression). Note that # PDF 1.2 also supports Flatedecode (zip compression). - bits = 8 params = None decode = None - smaskindata = 0 # # Get image characteristics width, height = im.size + dict_obj = {"BitsPerComponent": 8} if im.mode == "1": if features.check("libtiff"): filter = "CCITTFaxDecode" - bits = 1 + dict_obj["BitsPerComponent"] = 1 params = PdfParser.PdfArray( [ PdfParser.PdfDict( @@ -178,7 +177,7 @@ def _save(im, fp, filename, save_all=False): filter = "JPXDecode" colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images - smaskindata = 1 + dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": filter = "DCTDecode" colorspace = PdfParser.PdfName("DeviceCMYK") @@ -206,6 +205,7 @@ def _save(im, fp, filename, save_all=False): elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) elif filter == "JPXDecode": + del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) elif filter == "FlateDecode": ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) @@ -230,11 +230,10 @@ def _save(im, fp, filename, save_all=False): Width=width, # * 72.0 / x_resolution, Height=height, # * 72.0 / y_resolution, Filter=filter, - BitsPerComponent=bits, Decode=decode, DecodeParms=params, ColorSpace=colorspace, - SMaskInData=smaskindata, + **dict_obj, ) # From 6ca38552c98bcd3630f3f8bc4611f4ad6f0e342b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 3 Aug 2023 22:07:36 +1000 Subject: [PATCH 473/512] Do not set ColorSpace for JPXDecode since it is optional --- src/PIL/PdfImagePlugin.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index bcb0b3937..9566f0bc9 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -152,17 +152,17 @@ def _save(im, fp, filename, save_all=False): ) else: filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceGray") + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": filter = "DCTDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" - colorspace = PdfParser.PdfName("DeviceGray") + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "P": filter = "ASCIIHexDecode" palette = im.getpalette() - colorspace = [ + dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), 255, @@ -171,16 +171,15 @@ def _save(im, fp, filename, save_all=False): procset = "ImageI" # indexed color elif im.mode == "RGB": filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceRGB") + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images elif im.mode == "RGBA": filter = "JPXDecode" - colorspace = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": filter = "DCTDecode" - colorspace = PdfParser.PdfName("DeviceCMYK") + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] else: @@ -232,7 +231,6 @@ def _save(im, fp, filename, save_all=False): Filter=filter, Decode=decode, DecodeParms=params, - ColorSpace=colorspace, **dict_obj, ) From bc11b2d6a902bb17e555678b0a0bbba4ba486e98 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Aug 2023 18:46:58 +1000 Subject: [PATCH 474/512] Set SMaskInData to 1 for PDFs with alpha --- src/PIL/PdfImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 5d45461c5..fb3bac2ee 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -164,6 +164,7 @@ def _save(im, fp, filename, save_all=False): # params = f"<< /Predictor 15 /Columns {width-2} >>" colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale + dict_obj["SMaskInData"] = 1 elif im.mode == "P": filter = "ASCIIHexDecode" palette = im.getpalette() From c5b4ad94e894dd255a13b5ab733f8a64c24f5f93 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Aug 2023 18:47:28 +1000 Subject: [PATCH 475/512] Do not set ColorSpace for JPXDecode since it is optional --- src/PIL/PdfImagePlugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index fb3bac2ee..07f67d465 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -162,7 +162,6 @@ def _save(im, fp, filename, save_all=False): elif im.mode == "LA": filter = "JPXDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" - colorspace = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale dict_obj["SMaskInData"] = 1 elif im.mode == "P": From a5b025629023477ec62410ce77fd717c372d9fa2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Aug 2023 23:57:56 +1000 Subject: [PATCH 476/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f19bc346e..de5141d87 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Added saving LA images as PDFs #7299 + [radarhere] + +- Set SMaskInData to 1 for PDFs with alpha #7316, #7317 + [radarhere] + - Changed Image mode property to be read-only by default #7307 [radarhere] From c9147c9c85818c526ed2d8def7d39dcfc48a8cf0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 6 Aug 2023 22:14:32 +1000 Subject: [PATCH 477/512] Moved writing of object into separate function --- src/PIL/PdfImagePlugin.py | 248 +++++++++++++++++++------------------- 1 file changed, 127 insertions(+), 121 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 07f67d465..be39f4d16 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -46,6 +46,128 @@ def _save_all(im, fp, filename): # (Internal) Image save plugin for the PDF format. +def _write_image(im, filename, existing_pdf, image_refs): + # FIXME: Should replace ASCIIHexDecode with RunLengthDecode + # (packbits) or LZWDecode (tiff/lzw compression). Note that + # PDF 1.2 also supports Flatedecode (zip compression). + + params = None + decode = None + + # + # Get image characteristics + + width, height = im.size + + dict_obj = {"BitsPerComponent": 8} + if im.mode == "1": + if features.check("libtiff"): + filter = "CCITTFaxDecode" + dict_obj["BitsPerComponent"] = 1 + params = PdfParser.PdfArray( + [ + PdfParser.PdfDict( + { + "K": -1, + "BlackIs1": True, + "Columns": width, + "Rows": height, + } + ) + ] + ) + else: + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "L": + filter = "DCTDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") + procset = "ImageB" # grayscale + elif im.mode == "LA": + filter = "JPXDecode" + # params = f"<< /Predictor 15 /Columns {width-2} >>" + procset = "ImageB" # grayscale + dict_obj["SMaskInData"] = 1 + elif im.mode == "P": + filter = "ASCIIHexDecode" + palette = im.getpalette() + dict_obj["ColorSpace"] = [ + PdfParser.PdfName("Indexed"), + PdfParser.PdfName("DeviceRGB"), + 255, + PdfParser.PdfBinary(palette), + ] + procset = "ImageI" # indexed color + elif im.mode == "RGB": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") + procset = "ImageC" # color images + elif im.mode == "RGBA": + filter = "JPXDecode" + procset = "ImageC" # color images + dict_obj["SMaskInData"] = 1 + elif im.mode == "CMYK": + filter = "DCTDecode" + dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") + procset = "ImageC" # color images + decode = [1, 0, 1, 0, 1, 0, 1, 0] + else: + msg = f"cannot save mode {im.mode}" + raise ValueError(msg) + + # + # image + + op = io.BytesIO() + + if filter == "ASCIIHexDecode": + ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + elif filter == "CCITTFaxDecode": + im.save( + op, + "TIFF", + compression="group4", + # use a single strip + strip_size=math.ceil(im.width / 8) * im.height, + ) + elif filter == "DCTDecode": + Image.SAVE["JPEG"](im, op, filename) + elif filter == "JPXDecode": + del dict_obj["BitsPerComponent"] + Image.SAVE["JPEG2000"](im, op, filename) + elif filter == "FlateDecode": + ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) + elif filter == "RunLengthDecode": + ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) + else: + msg = f"unsupported PDF filter ({filter})" + raise ValueError(msg) + + stream = op.getvalue() + if filter == "CCITTFaxDecode": + stream = stream[8:] + filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + else: + filter = PdfParser.PdfName(filter) + + existing_pdf.write_obj( + image_refs[page_number], + stream=stream, + Type=PdfParser.PdfName("XObject"), + Subtype=PdfParser.PdfName("Image"), + Width=width, # * 72.0 / x_resolution, + Height=height, # * 72.0 / y_resolution, + Filter=filter, + Decode=decode, + DecodeParms=params, + **dict_obj, + ) + + return procset + + def _save(im, fp, filename, save_all=False): is_appending = im.encoderinfo.get("append", False) if is_appending: @@ -121,123 +243,7 @@ def _save(im, fp, filename, save_all=False): for im_sequence in ims: im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] for im in im_pages: - # FIXME: Should replace ASCIIHexDecode with RunLengthDecode - # (packbits) or LZWDecode (tiff/lzw compression). Note that - # PDF 1.2 also supports Flatedecode (zip compression). - - params = None - decode = None - - # - # Get image characteristics - - width, height = im.size - - dict_obj = {"BitsPerComponent": 8} - if im.mode == "1": - if features.check("libtiff"): - filter = "CCITTFaxDecode" - dict_obj["BitsPerComponent"] = 1 - params = PdfParser.PdfArray( - [ - PdfParser.PdfDict( - { - "K": -1, - "BlackIs1": True, - "Columns": width, - "Rows": height, - } - ) - ] - ) - else: - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "L": - filter = "DCTDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") - procset = "ImageB" # grayscale - elif im.mode == "LA": - filter = "JPXDecode" - # params = f"<< /Predictor 15 /Columns {width-2} >>" - procset = "ImageB" # grayscale - dict_obj["SMaskInData"] = 1 - elif im.mode == "P": - filter = "ASCIIHexDecode" - palette = im.getpalette() - dict_obj["ColorSpace"] = [ - PdfParser.PdfName("Indexed"), - PdfParser.PdfName("DeviceRGB"), - 255, - PdfParser.PdfBinary(palette), - ] - procset = "ImageI" # indexed color - elif im.mode == "RGB": - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") - procset = "ImageC" # color images - elif im.mode == "RGBA": - filter = "JPXDecode" - procset = "ImageC" # color images - dict_obj["SMaskInData"] = 1 - elif im.mode == "CMYK": - filter = "DCTDecode" - dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") - procset = "ImageC" # color images - decode = [1, 0, 1, 0, 1, 0, 1, 0] - else: - msg = f"cannot save mode {im.mode}" - raise ValueError(msg) - - # - # image - - op = io.BytesIO() - - if filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": - im.save( - op, - "TIFF", - compression="group4", - # use a single strip - strip_size=math.ceil(im.width / 8) * im.height, - ) - elif filter == "DCTDecode": - Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": - del dict_obj["BitsPerComponent"] - Image.SAVE["JPEG2000"](im, op, filename) - elif filter == "FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) - elif filter == "RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) - else: - msg = f"unsupported PDF filter ({filter})" - raise ValueError(msg) - - stream = op.getvalue() - if filter == "CCITTFaxDecode": - stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) - else: - filter = PdfParser.PdfName(filter) - - existing_pdf.write_obj( - image_refs[page_number], - stream=stream, - Type=PdfParser.PdfName("XObject"), - Subtype=PdfParser.PdfName("Image"), - Width=width, # * 72.0 / x_resolution, - Height=height, # * 72.0 / y_resolution, - Filter=filter, - Decode=decode, - DecodeParms=params, - **dict_obj, - ) + procset = _write_image(im, filename, existing_pdfs, image_refs) # # page @@ -251,8 +257,8 @@ def _save(im, fp, filename, save_all=False): MediaBox=[ 0, 0, - width * 72.0 / x_resolution, - height * 72.0 / y_resolution, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, ], Contents=contents_refs[page_number], ) @@ -261,8 +267,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 / x_resolution, - height * 72.0 / y_resolution, + im.width * 72.0 / x_resolution, + im.height * 72.0 / y_resolution, ) existing_pdf.write_obj(contents_refs[page_number], stream=page_contents) From a70ea82eb5c2cd073e1fe7cab0dca32b93fdcb9f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Aug 2023 13:53:19 +1000 Subject: [PATCH 478/512] Write P transparency as SMask --- Tests/test_file_pdf.py | 16 ++++++++++++++++ src/PIL/PdfImagePlugin.py | 21 ++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 9c8e90b7e..4f7b09af2 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -48,6 +48,22 @@ def test_save_alpha(tmp_path, mode): helper_save_as_pdf(tmp_path, mode) +def test_p_alpha(tmp_path): + # Arrange + outfile = str(tmp_path / "temp.pdf") + with Image.open("Tests/images/pil123p.png") as im: + assert im.mode == "P" + assert isinstance(im.info["transparency"], bytes) + + # Act + im.save(outfile) + + # Assert + with open(outfile, "rb") as fp: + contents = fp.read() + assert b"SMask" in contents + + def test_monochrome(tmp_path): # Arrange mode = "1" diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index be39f4d16..e3af1b452 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -100,6 +100,13 @@ def _write_image(im, filename, existing_pdf, image_refs): PdfParser.PdfBinary(palette), ] procset = "ImageI" # indexed color + + if "transparency" in im.info: + smask = im.convert("LA").getchannel("A") + smask.encoderinfo = {} + + image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] + dict_obj["SMask"] = image_ref elif im.mode == "RGB": filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") @@ -130,7 +137,7 @@ def _write_image(im, filename, existing_pdf, image_refs): "TIFF", compression="group4", # use a single strip - strip_size=math.ceil(im.width / 8) * im.height, + strip_size=math.ceil(width / 8) * height, ) elif filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) @@ -152,8 +159,9 @@ def _write_image(im, filename, existing_pdf, image_refs): else: filter = PdfParser.PdfName(filter) + image_ref = image_refs.pop(0) existing_pdf.write_obj( - image_refs[page_number], + image_ref, stream=stream, Type=PdfParser.PdfName("XObject"), Subtype=PdfParser.PdfName("Image"), @@ -165,7 +173,7 @@ def _write_image(im, filename, existing_pdf, image_refs): **dict_obj, ) - return procset + return image_ref, procset def _save(im, fp, filename, save_all=False): @@ -231,6 +239,9 @@ def _save(im, fp, filename, save_all=False): number_of_pages += im_number_of_pages for i in range(im_number_of_pages): image_refs.append(existing_pdf.next_object_id(0)) + if im.mode == "P" and "transparency" in im.info: + image_refs.append(existing_pdf.next_object_id(0)) + page_refs.append(existing_pdf.next_object_id(0)) contents_refs.append(existing_pdf.next_object_id(0)) existing_pdf.pages.append(page_refs[-1]) @@ -243,7 +254,7 @@ def _save(im, fp, filename, save_all=False): for im_sequence in ims: im_pages = ImageSequence.Iterator(im_sequence) if save_all else [im_sequence] for im in im_pages: - procset = _write_image(im, filename, existing_pdfs, image_refs) + image_ref, procset = _write_image(im, filename, existing_pdf, image_refs) # # page @@ -252,7 +263,7 @@ def _save(im, fp, filename, save_all=False): page_refs[page_number], Resources=PdfParser.PdfDict( ProcSet=[PdfParser.PdfName("PDF"), PdfParser.PdfName(procset)], - XObject=PdfParser.PdfDict(image=image_refs[page_number]), + XObject=PdfParser.PdfDict(image=image_ref), ), MediaBox=[ 0, From 5c5980721665a6a5ee64a5bf24efd26514b0b7eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Aug 2023 13:54:11 +1000 Subject: [PATCH 479/512] Removed unused decoders --- src/PIL/PdfImagePlugin.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e3af1b452..09fc0c7e6 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -144,10 +144,6 @@ def _write_image(im, filename, existing_pdf, image_refs): elif filter == "JPXDecode": del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) - elif filter == "FlateDecode": - ImageFile._save(im, op, [("zip", (0, 0) + im.size, 0, im.mode)]) - elif filter == "RunLengthDecode": - ImageFile._save(im, op, [("packbits", (0, 0) + im.size, 0, im.mode)]) else: msg = f"unsupported PDF filter ({filter})" raise ValueError(msg) From 73bd40babe644fcd402f0b7d3ae8be4894ca66f2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 7 Aug 2023 20:49:29 +1000 Subject: [PATCH 480/512] Test for relevant characters before and after "SMask" --- Tests/test_file_pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 4f7b09af2..ffc392d6b 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -61,7 +61,7 @@ def test_p_alpha(tmp_path): # Assert with open(outfile, "rb") as fp: contents = fp.read() - assert b"SMask" in contents + assert b"\n/SMask " in contents def test_monochrome(tmp_path): From 5b6b6346bb2736143df90249a6a6f3a985352d9a Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 7 Aug 2023 09:49:20 -0500 Subject: [PATCH 481/512] Fix param in test_image.py function --- Tests/test_image.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 36f24379a..7df1916ef 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -661,15 +661,15 @@ class TestImage: blank_p.palette = None blank_pa.palette = None - def _make_new(base_image, im, palette_result=None): - new_im = base_image._new(im) - assert new_im.mode == im.mode - assert new_im.size == im.size - assert new_im.info == base_image.info + def _make_new(base_image, image, palette_result=None): + new_image = base_image._new(image.im) + assert new_image.mode == image.mode + assert new_image.size == image.size + assert new_image.info == base_image.info if palette_result is not None: - assert new_im.palette.tobytes() == palette_result.tobytes() + assert new_image.palette.tobytes() == palette_result.tobytes() else: - assert new_im.palette is None + assert new_image.palette is None _make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3)) _make_new(im_p, im, None) From 24a19e5b37be9553584ba51d28e7aed8283c9d2d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:39:13 +0000 Subject: [PATCH 482/512] [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: 23.3.0 → 23.7.0](https://github.com/psf/black/compare/23.3.0...23.7.0) - [github.com/Lucas-C/pre-commit-hooks: v1.5.1 → v1.5.3](https://github.com/Lucas-C/pre-commit-hooks/compare/v1.5.1...v1.5.3) - [github.com/PyCQA/flake8: 6.0.0 → 6.1.0](https://github.com/PyCQA/flake8/compare/6.0.0...6.1.0) - [github.com/tox-dev/pyproject-fmt: 0.12.1 → 0.13.0](https://github.com/tox-dev/pyproject-fmt/compare/0.12.1...0.13.0) --- .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 320a77f55..5354509d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: [--target-version=py38] @@ -23,13 +23,13 @@ repos: - id: yesqa - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.1 + rev: v1.5.3 hooks: - id: remove-tabs exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$) - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: @@ -55,7 +55,7 @@ repos: - id: sphinx-lint - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.12.1 + rev: 0.13.0 hooks: - id: pyproject-fmt From 15930be644d97dd5d1c133b50c92d5ae411306f6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Aug 2023 08:44:03 +1000 Subject: [PATCH 483/512] Use "is" when comparing types --- src/PIL/ImageMath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index ac7d36b69..eb6bbe6c6 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -247,7 +247,7 @@ def eval(expression, _dict={}, **kw): def scan(code): for const in code.co_consts: - if type(const) == type(compiled_code): + if type(const) is type(compiled_code): scan(const) for name in code.co_names: From c98a7994da83a416bfd0efc068bc7ebf5fc1d133 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 8 Aug 2023 21:33:55 +1000 Subject: [PATCH 484/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index de5141d87..937103c66 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Fixed transparency when saving P mode images to PDF #7323 + [radarhere] + - Added saving LA images as PDFs #7299 [radarhere] From bfafa460e3e2710e840960a8d928fe44dc326874 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Aug 2023 10:31:34 +1000 Subject: [PATCH 485/512] Allow "loop=None" when saving --- Tests/test_file_gif.py | 8 ++++++++ docs/handbook/image-file-formats.rst | 2 +- src/PIL/GifImagePlugin.py | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index b1c9f731f..d571692b1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -875,6 +875,14 @@ def test_identical_frames_to_single_frame(duration, tmp_path): assert reread.info["duration"] == 8500 +def test_loop_none(tmp_path): + out = str(tmp_path / "temp.gif") + im = Image.new("L", (100, 100), "#000") + im.save(out, loop=None) + with Image.open(out) as reread: + assert "loop" not in reread.info + + def test_number_of_loops(tmp_path): number_of_loops = 2 diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 0d8d9bc10..2a42bdacb 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -253,7 +253,7 @@ their :py:attr:`~PIL.Image.Image.info` values. **loop** Integer number of times the GIF should loop. 0 means that it will loop - forever. By default, the image will not loop. + forever. If omitted or ``None``, the image will not loop. **comment** A comment about the image. diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 943842f77..92074b0d4 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -912,7 +912,7 @@ def _get_global_header(im, info): info and ( "transparency" in info - or "loop" in info + or info.get("loop") is not None or info.get("duration") or info.get("comment") ) @@ -937,7 +937,7 @@ def _get_global_header(im, info): # Global Color Table _get_header_palette(palette_bytes), ] - if "loop" in info: + if info.get("loop") is not None: header.append( b"!" + o8(255) # extension intro From f39f74fb82348ce87dfc9e4766ee473132ce84d3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Aug 2023 07:42:24 +1000 Subject: [PATCH 486/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 937103c66..b02c33ac3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Allow "loop=None" when saving GIF images #7329 + [radarhere] + - Fixed transparency when saving P mode images to PDF #7323 [radarhere] From bc658e17917ff8959e4db752c8d12082a5cbd8d1 Mon Sep 17 00:00:00 2001 From: TheNooB Date: Fri, 11 Aug 2023 17:47:36 +0800 Subject: [PATCH 487/512] Add session type check in grabclipboard for Linux --- src/PIL/ImageGrab.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 927033c60..c23b40295 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -140,7 +140,24 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - if shutil.which("wl-paste"): + if shutil.which("loginctl"): + try: + loginctl = subprocess.check_output("loginctl").decode().split("\n") + except subprocess.CalledProcessError: + loginctl = None + else: + loginctl = None + + if loginctl is not None: + username = subprocess.check_output("whoami").decode().strip("\n") + sessionid = [line.split()[0] for line in loginctl if username in line.split()][0] + sessiontype = subprocess.check_output( + ["loginctl", "show-session", sessionid, "-p", "Type"] + ).decode().strip("\n").split("=")[1] + else: # Session type check failed + sessiontype = None + + if shutil.which("wl-paste") and ((sessiontype == "wayland") or (sessiontype is None)): output = subprocess.check_output(["wl-paste", "-l"]).decode() mimetypes = output.splitlines() if "image/png" in mimetypes: @@ -153,11 +170,12 @@ def grabclipboard(): args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) - elif shutil.which("xclip"): + elif shutil.which("xclip") and ((sessiontype == "x11") or (sessiontype is None)): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" raise NotImplementedError(msg) + p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = p.stderr if err: From 08b538780d09303d617e5e4fb68ba1d9e34f5629 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 11 Aug 2023 19:49:02 +1000 Subject: [PATCH 488/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b02c33ac3..e752f66c4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Read WebP duration after opening #7311 + [k128, radarhere] + - Allow "loop=None" when saving GIF images #7329 [radarhere] From 164ea2df6f60c6071088c683b48f1a0c686a30a3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:42:09 +0000 Subject: [PATCH 489/512] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/ImageGrab.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c23b40295..44bc6d38f 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -146,18 +146,27 @@ def grabclipboard(): except subprocess.CalledProcessError: loginctl = None else: - loginctl = None + loginctl = None if loginctl is not None: username = subprocess.check_output("whoami").decode().strip("\n") - sessionid = [line.split()[0] for line in loginctl if username in line.split()][0] - sessiontype = subprocess.check_output( - ["loginctl", "show-session", sessionid, "-p", "Type"] - ).decode().strip("\n").split("=")[1] - else: # Session type check failed + sessionid = [ + line.split()[0] for line in loginctl if username in line.split() + ][0] + sessiontype = ( + subprocess.check_output( + ["loginctl", "show-session", sessionid, "-p", "Type"] + ) + .decode() + .strip("\n") + .split("=")[1] + ) + else: # Session type check failed sessiontype = None - if shutil.which("wl-paste") and ((sessiontype == "wayland") or (sessiontype is None)): + if shutil.which("wl-paste") and ( + (sessiontype == "wayland") or (sessiontype is None) + ): output = subprocess.check_output(["wl-paste", "-l"]).decode() mimetypes = output.splitlines() if "image/png" in mimetypes: @@ -170,7 +179,9 @@ def grabclipboard(): args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) - elif shutil.which("xclip") and ((sessiontype == "x11") or (sessiontype is None)): + elif shutil.which("xclip") and ( + (sessiontype == "x11") or (sessiontype is None) + ): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" From a8b3feac86c0800968a52db1ff48477076f106c4 Mon Sep 17 00:00:00 2001 From: TheNooB <73348767+TheNooB2706@users.noreply.github.com> Date: Fri, 11 Aug 2023 21:01:05 +0800 Subject: [PATCH 490/512] Apply suggestions from code review Simplify conditional expressions Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 44bc6d38f..c510c835f 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -164,9 +164,7 @@ def grabclipboard(): else: # Session type check failed sessiontype = None - if shutil.which("wl-paste") and ( - (sessiontype == "wayland") or (sessiontype is None) - ): + if shutil.which("wl-paste") and sessiontype in ["wayland", None]: output = subprocess.check_output(["wl-paste", "-l"]).decode() mimetypes = output.splitlines() if "image/png" in mimetypes: @@ -179,9 +177,7 @@ def grabclipboard(): args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) - elif shutil.which("xclip") and ( - (sessiontype == "x11") or (sessiontype is None) - ): + elif shutil.which("xclip") and sessiontype in ["x11", None]: args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" From 7b157b045aa3f7b5ca61d94b25ed03479759325e Mon Sep 17 00:00:00 2001 From: TheNooB Date: Fri, 11 Aug 2023 21:14:34 +0800 Subject: [PATCH 491/512] Use os.getlogin() instead of whoami command for getting username --- src/PIL/ImageGrab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index c510c835f..07540a1db 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -149,7 +149,7 @@ def grabclipboard(): loginctl = None if loginctl is not None: - username = subprocess.check_output("whoami").decode().strip("\n") + username = os.getlogin() sessionid = [ line.split()[0] for line in loginctl if username in line.split() ][0] From c167d7a269248c93d26c123a1b200c93f1cffd4e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Aug 2023 12:09:20 +1000 Subject: [PATCH 492/512] Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii --- Tests/test_box_blur.py | 2 +- Tests/test_image_filter.py | 17 ++++++++++++++--- src/PIL/ImageFilter.py | 26 +++++++++++++++++++------- src/_imaging.c | 12 ++++++------ src/libImaging/BoxBlur.c | 27 +++++++++++++++++++-------- src/libImaging/Imaging.h | 4 ++-- src/libImaging/UnsharpMask.c | 2 +- 7 files changed, 62 insertions(+), 28 deletions(-) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index 3bdd5177d..745364ddc 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -22,7 +22,7 @@ def test_imageops_box_blur(): def box_blur(image, radius=1, n=1): - return image._new(image.im.box_blur(radius, n)) + return image._new(image.im.box_blur((radius, radius), n)) def assert_image(im, data, delta=0): diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 25b72298e..521551212 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -24,8 +24,10 @@ from .helper import assert_image_equal, hopper ImageFilter.ModeFilter, ImageFilter.GaussianBlur, ImageFilter.GaussianBlur(5), + ImageFilter.GaussianBlur((2, 5)), ImageFilter.BoxBlur(0), ImageFilter.BoxBlur(5), + ImageFilter.BoxBlur((2, 5)), ImageFilter.UnsharpMask, ImageFilter.UnsharpMask(10), ), @@ -185,12 +187,21 @@ def test_consistency_5x5(mode): assert_image_equal(source.filter(kernel), reference) -def test_invalid_box_blur_filter(): +@pytest.mark.parametrize( + "radius", + ( + -2, + (-2, -2), + (-2, 2), + (2, -2), + ), +) +def test_invalid_box_blur_filter(radius): with pytest.raises(ValueError): - ImageFilter.BoxBlur(-2) + ImageFilter.BoxBlur(radius) im = hopper() box_blur_filter = ImageFilter.BoxBlur(2) - box_blur_filter.radius = -2 + box_blur_filter.radius = radius with pytest.raises(ValueError): im.filter(box_blur_filter) diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 33bc7cc2e..0d2fec9ee 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -157,7 +157,8 @@ class GaussianBlur(MultibandFilter): approximates a Gaussian kernel. For details on accuracy see - :param radius: Standard deviation of the Gaussian kernel. + :param radius: Standard deviation of the Gaussian kernel. Either a sequence of two + numbers for x and y, or a single number for both. """ name = "GaussianBlur" @@ -166,7 +167,10 @@ class GaussianBlur(MultibandFilter): self.radius = radius def filter(self, image): - return image.gaussian_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + return image.gaussian_blur(xy) class BoxBlur(MultibandFilter): @@ -176,21 +180,29 @@ class BoxBlur(MultibandFilter): which runs in linear time relative to the size of the image for any radius value. - :param radius: Size of the box in one direction. Radius 0 does not blur, - returns an identical image. Radius 1 takes 1 pixel - in each direction, i.e. 9 pixels in total. + :param radius: Size of the box in a direction. Either a sequence of two numbers for + x and y, or a single number for both. + + Radius 0 does not blur, returns an identical image. + Radius 1 takes 1 pixel in each direction, i.e. 9 pixels in total. """ name = "BoxBlur" def __init__(self, radius): - if radius < 0: + xy = radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + if xy[0] < 0 or xy[1] < 0: msg = "radius must be >= 0" raise ValueError(msg) self.radius = radius def filter(self, image): - return image.box_blur(self.radius) + xy = self.radius + if not isinstance(xy, (tuple, list)): + xy = (xy, xy) + return image.box_blur(xy) class UnsharpMask(MultibandFilter): diff --git a/src/_imaging.c b/src/_imaging.c index e15cb89fc..95da2772d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1075,9 +1075,9 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius = 0; + float xradius, yradius; int passes = 3; - if (!PyArg_ParseTuple(args, "f|i", &radius, &passes)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &passes)) { return NULL; } @@ -1087,7 +1087,7 @@ _gaussian_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingGaussianBlur(imOut, imIn, radius, passes)) { + if (!ImagingGaussianBlur(imOut, imIn, xradius, yradius, passes)) { ImagingDelete(imOut); return NULL; } @@ -2131,9 +2131,9 @@ _box_blur(ImagingObject *self, PyObject *args) { Imaging imIn; Imaging imOut; - float radius; + float xradius, yradius; int n = 1; - if (!PyArg_ParseTuple(args, "f|i", &radius, &n)) { + if (!PyArg_ParseTuple(args, "(ff)|i", &xradius, &yradius, &n)) { return NULL; } @@ -2143,7 +2143,7 @@ _box_blur(ImagingObject *self, PyObject *args) { return NULL; } - if (!ImagingBoxBlur(imOut, imIn, radius, n)) { + if (!ImagingBoxBlur(imOut, imIn, xradius, yradius, n)) { ImagingDelete(imOut); return NULL; } diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 5afe7cf50..41e9fbed9 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -230,14 +230,14 @@ ImagingHorizontalBoxBlur(Imaging imOut, Imaging imIn, float floatRadius) { } Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) { int i; Imaging imTransposed; if (n < 1) { return ImagingError_ValueError("number of passes must be greater than zero"); } - if (radius < 0) { + if (xradius < 0 || yradius < 0) { return ImagingError_ValueError("radius must be >= 0"); } @@ -266,16 +266,16 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { /* Apply blur in one dimension. Use imOut as a destination at first pass, then use imOut as a source too. */ - ImagingHorizontalBoxBlur(imOut, imIn, radius); + ImagingHorizontalBoxBlur(imOut, imIn, xradius); for (i = 1; i < n; i++) { - ImagingHorizontalBoxBlur(imOut, imOut, radius); + ImagingHorizontalBoxBlur(imOut, imOut, xradius); } /* Transpose result for blur in another direction. */ ImagingTranspose(imTransposed, imOut); /* Reuse imTransposed as a source and destination there. */ for (i = 0; i < n; i++) { - ImagingHorizontalBoxBlur(imTransposed, imTransposed, radius); + ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); } /* Restore original orientation. */ ImagingTranspose(imOut, imTransposed); @@ -285,8 +285,8 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n) { return imOut; } -Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { +static float +_gaussian_blur_radius(float radius, int passes) { float sigma2, L, l, a; sigma2 = radius * radius / passes; @@ -299,5 +299,16 @@ ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes) { a = (2 * l + 1) * (l * (l + 1) - 3 * sigma2); a /= 6 * (sigma2 - (l + 1) * (l + 1)); - return ImagingBoxBlur(imOut, imIn, l + a, passes); + return l + a; +} + +Imaging +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes) { + return ImagingBoxBlur( + imOut, + imIn, + _gaussian_blur_radius(xradius, passes), + _gaussian_blur_radius(yradius, passes), + passes + ); } diff --git a/src/libImaging/Imaging.h b/src/libImaging/Imaging.h index 01f40ee7b..afcd2229b 100644 --- a/src/libImaging/Imaging.h +++ b/src/libImaging/Imaging.h @@ -309,7 +309,7 @@ ImagingFlipLeftRight(Imaging imOut, Imaging imIn); extern Imaging ImagingFlipTopBottom(Imaging imOut, Imaging imIn); extern Imaging -ImagingGaussianBlur(Imaging imOut, Imaging imIn, float radius, int passes); +ImagingGaussianBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int passes); extern Imaging ImagingGetBand(Imaging im, int band); extern Imaging @@ -376,7 +376,7 @@ ImagingTransform( extern Imaging ImagingUnsharpMask(Imaging imOut, Imaging im, float radius, int percent, int threshold); extern Imaging -ImagingBoxBlur(Imaging imOut, Imaging imIn, float radius, int n); +ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n); extern Imaging ImagingColorLUT3D_linear( Imaging imOut, diff --git a/src/libImaging/UnsharpMask.c b/src/libImaging/UnsharpMask.c index 643ced49f..2853ce903 100644 --- a/src/libImaging/UnsharpMask.c +++ b/src/libImaging/UnsharpMask.c @@ -36,7 +36,7 @@ ImagingUnsharpMask( /* First, do a gaussian blur on the image, putting results in imOut temporarily. All format checks are in gaussian blur. */ - result = ImagingGaussianBlur(imOut, imIn, radius, 3); + result = ImagingGaussianBlur(imOut, imIn, radius, radius, 3); if (!result) { return NULL; } From 0b6ab7914562b764dad45c620a8c36e9cabfe070 Mon Sep 17 00:00:00 2001 From: TheNooB Date: Sat, 12 Aug 2023 12:51:09 +0800 Subject: [PATCH 493/512] Check session type using environment variable instead of loginctl --- src/PIL/ImageGrab.py | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 07540a1db..07b617cc3 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -140,27 +140,10 @@ def grabclipboard(): return BmpImagePlugin.DibImageFile(data) return None else: - if shutil.which("loginctl"): - try: - loginctl = subprocess.check_output("loginctl").decode().split("\n") - except subprocess.CalledProcessError: - loginctl = None - else: - loginctl = None - - if loginctl is not None: - username = os.getlogin() - sessionid = [ - line.split()[0] for line in loginctl if username in line.split() - ][0] - sessiontype = ( - subprocess.check_output( - ["loginctl", "show-session", sessionid, "-p", "Type"] - ) - .decode() - .strip("\n") - .split("=")[1] - ) + if os.getenv("WAYLAND_DISPLAY"): + sessiontype = "wayland" + elif os.getenv("DISPLAY"): + sessiontype = "x11" else: # Session type check failed sessiontype = None From 9f54a11a9c96b4207fa57c65ed401c3e30d499e9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 12 Aug 2023 18:17:07 +1000 Subject: [PATCH 494/512] Improve efficiency when a radius is zero --- Tests/test_image_filter.py | 1 + src/PIL/ImageFilter.py | 4 ++++ src/libImaging/BoxBlur.c | 44 +++++++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 521551212..a7932a351 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -23,6 +23,7 @@ from .helper import assert_image_equal, hopper ImageFilter.MinFilter, ImageFilter.ModeFilter, ImageFilter.GaussianBlur, + ImageFilter.GaussianBlur(0), ImageFilter.GaussianBlur(5), ImageFilter.GaussianBlur((2, 5)), ImageFilter.BoxBlur(0), diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index 0d2fec9ee..57268b8f5 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -170,6 +170,8 @@ class GaussianBlur(MultibandFilter): xy = self.radius if not isinstance(xy, (tuple, list)): xy = (xy, xy) + if xy == (0, 0): + return image.copy() return image.gaussian_blur(xy) @@ -202,6 +204,8 @@ class BoxBlur(MultibandFilter): xy = self.radius if not isinstance(xy, (tuple, list)): xy = (xy, xy) + if xy == (0, 0): + return image.copy() return image.box_blur(xy) diff --git a/src/libImaging/BoxBlur.c b/src/libImaging/BoxBlur.c index 41e9fbed9..adf425d0d 100644 --- a/src/libImaging/BoxBlur.c +++ b/src/libImaging/BoxBlur.c @@ -258,29 +258,39 @@ ImagingBoxBlur(Imaging imOut, Imaging imIn, float xradius, float yradius, int n) return ImagingError_ModeError(); } - imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); - if (!imTransposed) { - return NULL; - } - /* Apply blur in one dimension. Use imOut as a destination at first pass, then use imOut as a source too. */ - ImagingHorizontalBoxBlur(imOut, imIn, xradius); - for (i = 1; i < n; i++) { - ImagingHorizontalBoxBlur(imOut, imOut, xradius); - } - /* Transpose result for blur in another direction. */ - ImagingTranspose(imTransposed, imOut); - /* Reuse imTransposed as a source and destination there. */ - for (i = 0; i < n; i++) { - ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); + if (xradius != 0) { + ImagingHorizontalBoxBlur(imOut, imIn, xradius); + for (i = 1; i < n; i++) { + ImagingHorizontalBoxBlur(imOut, imOut, xradius); + } } - /* Restore original orientation. */ - ImagingTranspose(imOut, imTransposed); + if (yradius != 0) { + imTransposed = ImagingNewDirty(imIn->mode, imIn->ysize, imIn->xsize); + if (!imTransposed) { + return NULL; + } - ImagingDelete(imTransposed); + /* Transpose result for blur in another direction. */ + ImagingTranspose(imTransposed, xradius == 0 ? imIn : imOut); + + /* Reuse imTransposed as a source and destination there. */ + for (i = 0; i < n; i++) { + ImagingHorizontalBoxBlur(imTransposed, imTransposed, yradius); + } + /* Restore original orientation. */ + ImagingTranspose(imOut, imTransposed); + + ImagingDelete(imTransposed); + } + if (xradius == 0 && yradius == 0) { + if (!ImagingCopy2(imOut, imIn)) { + return NULL; + } + } return imOut; } From e06edcb527a94c7095d3ea008a6f77ef793f7adc Mon Sep 17 00:00:00 2001 From: TheNooB <73348767+TheNooB2706@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:33:36 +0800 Subject: [PATCH 495/512] Reformat variable name following PEP8 Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/ImageGrab.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 07b617cc3..6dbdd0abc 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -141,13 +141,13 @@ def grabclipboard(): return None else: if os.getenv("WAYLAND_DISPLAY"): - sessiontype = "wayland" + session_type = "wayland" elif os.getenv("DISPLAY"): - sessiontype = "x11" + session_type = "x11" else: # Session type check failed - sessiontype = None + session_type = None - if shutil.which("wl-paste") and sessiontype in ["wayland", None]: + if shutil.which("wl-paste") and session_type in ["wayland", None]: output = subprocess.check_output(["wl-paste", "-l"]).decode() mimetypes = output.splitlines() if "image/png" in mimetypes: @@ -160,7 +160,7 @@ def grabclipboard(): args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) - elif shutil.which("xclip") and sessiontype in ["x11", None]: + elif shutil.which("xclip") and session_type in ["x11", None]: args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" From 8587121932afff43c12eef05d6f212721f9ea5e5 Mon Sep 17 00:00:00 2001 From: Tommy <4850853+wx00@users.noreply.github.com> Date: Fri, 18 Aug 2023 11:20:18 +0800 Subject: [PATCH 496/512] Fixed a typo in 10.0.0 release note --- docs/releasenotes/10.0.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releasenotes/10.0.0.rst b/docs/releasenotes/10.0.0.rst index 4cd629322..06acfc7af 100644 --- a/docs/releasenotes/10.0.0.rst +++ b/docs/releasenotes/10.0.0.rst @@ -206,4 +206,4 @@ Support reading signed 8-bit TIFF images ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TIFF images with signed integer data, 8 bits per sample and a photometric -interpretaton of BlackIsZero can now be read. +interpretation of BlackIsZero can now be read. From 9ef7cb39def45b0fe1cdf4828ca20838a1fc39d1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 18 Aug 2023 22:22:51 +1000 Subject: [PATCH 497/512] Updated zlib to 1.3 --- Tests/test_file_png.py | 2 +- winbuild/build_prepare.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 3ffe93c6d..f8df88d67 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -79,7 +79,7 @@ class TestFilePng: def test_sanity(self, tmp_path): # internal version number - assert re.search(r"\d+\.\d+\.\d+(\.\d+)?$", features.version_codec("zlib")) + assert re.search(r"\d+(\.\d+){1,3}$", features.version_codec("zlib")) test_file = str(tmp_path / "temp.png") diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index f24a013a4..412f30026 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -130,9 +130,9 @@ deps = { "bins": ["cjpeg.exe", "djpeg.exe"], }, "zlib": { - "url": "https://zlib.net/zlib1213.zip", - "filename": "zlib1213.zip", - "dir": "zlib-1.2.13", + "url": "https://zlib.net/zlib13.zip", + "filename": "zlib13.zip", + "dir": "zlib-1.3", "license": "README", "license_pattern": "Copyright notice:\n\n(.+)$", "build": [ From a04ba81e22360cbb9009efaf294581e0c7350947 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Aug 2023 12:37:00 +1000 Subject: [PATCH 498/512] bufsize is already increased to MAXBLOCK in ImageFile._save() --- src/PIL/JpegImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 475e54c2b..fe921eb65 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -800,7 +800,7 @@ def _save(im, fp, filename): # The EXIF info needs to be written as one block, + APP1, + one spare byte. # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(ImageFile.MAXBLOCK, bufsize, len(exif) + 5, len(extra) + 1) + bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) From 0a28840bc4ba8f9f40be4b63a9e7754dd6a697fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 19 Aug 2023 14:34:08 +1000 Subject: [PATCH 499/512] Expand buffer size when optimizing or progressive --- Tests/test_file_jpeg.py | 9 ++++++++- src/PIL/JpegImagePlugin.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 1dc82ef1e..b62132613 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -214,13 +214,20 @@ class TestFileJpeg: # Should not raise OSError for image with icc larger than image size. im.save( f, - format="JPEG", progressive=True, quality=95, icc_profile=icc_profile, optimize=True, ) + with Image.open("Tests/images/flower2.jpg") as im: + f = str(tmp_path / "temp2.jpg") + im.save(f, progressive=True, quality=94, icc_profile=b" " * 53955) + + with Image.open("Tests/images/flower2.jpg") as im: + f = str(tmp_path / "temp3.jpg") + im.save(f, progressive=True, quality=94, exif=b" " * 43668) + def test_optimize(self): im1 = self.roundtrip(hopper()) im2 = self.roundtrip(hopper(), optimize=0) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fe921eb65..74130854f 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -797,10 +797,14 @@ def _save(im, fp, filename): bufsize = 2 * im.size[0] * im.size[1] else: bufsize = im.size[0] * im.size[1] - - # The EXIF info needs to be written as one block, + APP1, + one spare byte. - # Ensure that our buffer is big enough. Same with the icc_profile block. - bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) + if exif: + bufsize += len(exif) + 5 + if extra: + bufsize += len(extra) + 1 + else: + # The EXIF info needs to be written as one block, + APP1, + one spare byte. + # Ensure that our buffer is big enough. Same with the icc_profile block. + bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) From 115ef3a36df58588d53127c0bbe717af13ca691c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 21 Aug 2023 15:17:30 +1000 Subject: [PATCH 500/512] 32-bit Windows wheels are no longer provided --- docs/installation.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index a4d22316d..724348c13 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -83,10 +83,9 @@ Install Pillow with :command:`pip`:: .. tab:: Windows We provide Pillow binaries for Windows compiled for the matrix of - supported Pythons in both 32 and 64-bit versions in the wheel format. - These binaries include support for all optional libraries except - libimagequant and libxcb. Raqm support requires - FriBiDi to be installed separately:: + supported Pythons in 64-bit versions in the wheel format. These binaries include + support for all optional libraries except libimagequant and libxcb. Raqm support + requires FriBiDi to be installed separately:: python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow From 9c3bc70f667e105c8bce5f34d8895ac5fcfbd02f Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Tue, 22 Aug 2023 10:31:28 +1000 Subject: [PATCH 501/512] Use tuples Co-authored-by: Hugo van Kemenade --- src/PIL/ImageGrab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageGrab.py b/src/PIL/ImageGrab.py index 6dbdd0abc..43019f74a 100644 --- a/src/PIL/ImageGrab.py +++ b/src/PIL/ImageGrab.py @@ -147,7 +147,7 @@ def grabclipboard(): else: # Session type check failed session_type = None - if shutil.which("wl-paste") and session_type in ["wayland", None]: + if shutil.which("wl-paste") and session_type in ("wayland", None): output = subprocess.check_output(["wl-paste", "-l"]).decode() mimetypes = output.splitlines() if "image/png" in mimetypes: @@ -160,7 +160,7 @@ def grabclipboard(): args = ["wl-paste"] if mimetype: args.extend(["-t", mimetype]) - elif shutil.which("xclip") and session_type in ["x11", None]: + elif shutil.which("xclip") and session_type in ("x11", None): args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"] else: msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux" From 3ea8b54c00fb4aec64873c3e817019c85fbd217f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 22 Aug 2023 12:08:50 +1000 Subject: [PATCH 502/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e752f66c4..beabd4828 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Added session type check for Linux in ImageGrab.grabclipboard() #7332 + [TheNooB2706, radarhere, hugovk] + - Read WebP duration after opening #7311 [k128, radarhere] From 472eb666833f97e5d44364a1e80dec355623451e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Aug 2023 19:02:27 +1000 Subject: [PATCH 503/512] Skip tests that require FreeType if FreeType is not available --- Tests/oss-fuzz/test_fuzzers.py | 2 ++ Tests/test_imagedraw.py | 1 + Tests/test_pickle.py | 2 ++ 3 files changed, 5 insertions(+) diff --git a/Tests/oss-fuzz/test_fuzzers.py b/Tests/oss-fuzz/test_fuzzers.py index dc111c38b..0526f550e 100644 --- a/Tests/oss-fuzz/test_fuzzers.py +++ b/Tests/oss-fuzz/test_fuzzers.py @@ -6,6 +6,7 @@ import packaging import pytest from PIL import Image, features +from Tests.helper import skip_unless_feature if sys.platform.startswith("win32"): pytest.skip("Fuzzer is linux only", allow_module_level=True) @@ -48,6 +49,7 @@ def test_fuzz_images(path): fuzzers.disable_decompressionbomb_error() +@skip_unless_feature("freetype2") @pytest.mark.parametrize( "path", subprocess.check_output("find Tests/fonts -type f", shell=True).split(b"\n") ) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 7497fdc66..ca6235447 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1326,6 +1326,7 @@ def test_stroke_multiline(): assert_image_similar_tofile(im, "Tests/images/imagedraw_stroke_multiline.png", 3.3) +@skip_unless_feature("freetype2") def test_setting_default_font(): # Arrange im = Image.new("RGB", (100, 250)) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index 531936f8f..1c5d482bd 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -112,6 +112,7 @@ def helper_assert_pickled_font_images(font1, font2): assert_image_equal(im1, im2) +@skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_string(protocol): # Arrange @@ -125,6 +126,7 @@ def test_pickle_font_string(protocol): helper_assert_pickled_font_images(font, unpickled_font) +@skip_unless_feature("freetype2") @pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_file(tmp_path, protocol): # Arrange From 8fee27340d475cdfd3dfbc637f1bb0a6954c4ba4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 25 Aug 2023 19:49:10 +1000 Subject: [PATCH 504/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index beabd4828..cbacf5bde 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 + [radarhere] + - Added session type check for Linux in ImageGrab.grabclipboard() #7332 [TheNooB2706, radarhere, hugovk] From 823178d4c270629cb6bfbe233fba08eb3054fabe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Aug 2023 13:28:53 +1000 Subject: [PATCH 505/512] Updated freetype to 2.13.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 c94d8a363..812799ac2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -235,9 +235,9 @@ deps = { "libs": ["*.lib"], }, "freetype": { - "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.1.tar.gz", # noqa: E501 - "filename": "freetype-2.13.1.tar.gz", - "dir": "freetype-2.13.1", + "url": "https://download.savannah.gnu.org/releases/freetype/freetype-2.13.2.tar.gz", # noqa: E501 + "filename": "freetype-2.13.2.tar.gz", + "dir": "freetype-2.13.2", "license": ["LICENSE.TXT", r"docs\FTL.TXT", r"docs\GPLv2.TXT"], "patch": { r"builds\windows\vc2010\freetype.vcxproj": { From 091ab74429b9c2c38c67c65d2038af242a47c870 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Aug 2023 18:22:04 +1000 Subject: [PATCH 506/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cbacf5bde..c437e7940 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,9 @@ Changelog (Pillow) - Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 [radarhere] +- Expand JPEG buffer size when saving optimized or progressive #7345 + [radarhere] + - Added session type check for Linux in ImageGrab.grabclipboard() #7332 [TheNooB2706, radarhere, hugovk] From d8c3135b6bcff0105453346dfa35886615bf0505 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Aug 2023 20:12:23 +1000 Subject: [PATCH 507/512] Allow getpixel to accept a list --- Tests/test_image_access.py | 4 ++++ src/_imaging.c | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index c9db3aee7..f80bc9c10 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -213,6 +213,10 @@ class TestImageGetPixel(AccessTest): def test_basic(self, mode): self.check(mode) + def test_list(self): + im = hopper() + assert im.getpixel([0, 0]) == (20, 20, 70) + @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize( "expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1) diff --git a/src/_imaging.c b/src/_imaging.c index 95da2772d..736f347a3 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1146,11 +1146,15 @@ static inline int _getxy(PyObject *xy, int *x, int *y) { PyObject *value; - if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { + int tuple = PyTuple_Check(xy); + if ( + !(tuple && PyTuple_GET_SIZE(xy) == 2) && + !(PyList_Check(xy) && PyList_GET_SIZE(xy) == 2) + ) { goto badarg; } - value = PyTuple_GET_ITEM(xy, 0); + value = tuple ? PyTuple_GET_ITEM(xy, 0) : PyList_GET_ITEM(xy, 0); if (PyLong_Check(value)) { *x = PyLong_AS_LONG(value); } else if (PyFloat_Check(value)) { @@ -1164,7 +1168,7 @@ _getxy(PyObject *xy, int *x, int *y) { } } - value = PyTuple_GET_ITEM(xy, 1); + value = tuple ? PyTuple_GET_ITEM(xy, 1) : PyList_GET_ITEM(xy, 1); if (PyLong_Check(value)) { *y = PyLong_AS_LONG(value); } else if (PyFloat_Check(value)) { From 69a81dd8673f7c29f24d16061d2d7a871405333c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 28 Aug 2023 22:43:20 +1000 Subject: [PATCH 508/512] Convert list to tuple in Python instead of C --- src/PIL/Image.py | 2 ++ src/_imaging.c | 10 +++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 476ed0122..2b7ec6bec 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1565,6 +1565,8 @@ class Image: self.load() if self.pyaccess: return self.pyaccess.getpixel(xy) + if isinstance(xy, list): + xy = tuple(xy) return self.im.getpixel(xy) def getprojection(self): diff --git a/src/_imaging.c b/src/_imaging.c index 736f347a3..95da2772d 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -1146,15 +1146,11 @@ static inline int _getxy(PyObject *xy, int *x, int *y) { PyObject *value; - int tuple = PyTuple_Check(xy); - if ( - !(tuple && PyTuple_GET_SIZE(xy) == 2) && - !(PyList_Check(xy) && PyList_GET_SIZE(xy) == 2) - ) { + if (!PyTuple_Check(xy) || PyTuple_GET_SIZE(xy) != 2) { goto badarg; } - value = tuple ? PyTuple_GET_ITEM(xy, 0) : PyList_GET_ITEM(xy, 0); + value = PyTuple_GET_ITEM(xy, 0); if (PyLong_Check(value)) { *x = PyLong_AS_LONG(value); } else if (PyFloat_Check(value)) { @@ -1168,7 +1164,7 @@ _getxy(PyObject *xy, int *x, int *y) { } } - value = tuple ? PyTuple_GET_ITEM(xy, 1) : PyList_GET_ITEM(xy, 1); + value = PyTuple_GET_ITEM(xy, 1); if (PyLong_Check(value)) { *y = PyLong_AS_LONG(value); } else if (PyFloat_Check(value)) { From f9f367fe54c68e856a5630462f6cbad4ce49c186 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 30 Aug 2023 07:27:09 +1000 Subject: [PATCH 509/512] Always cast to a tuple Co-authored-by: Alexander Karpinsky --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 2b7ec6bec..6ea711b56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1565,9 +1565,7 @@ class Image: self.load() if self.pyaccess: return self.pyaccess.getpixel(xy) - if isinstance(xy, list): - xy = tuple(xy) - return self.im.getpixel(xy) + return self.im.getpixel(tuple(xy)) def getprojection(self): """ From 24606216e1e5931a8fe6f41acde9e7e67489905d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Aug 2023 20:59:26 +1000 Subject: [PATCH 510/512] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c437e7940..4a7b5ba5e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 10.1.0 (unreleased) ------------------- +- Allow getpixel() to accept a list #7355 + [radarhere, homm] + - Allow GaussianBlur and BoxBlur to accept a sequence of x and y radii #7336 [radarhere] From c0eb1bdeeb8bff749f7114f32d50cfc77f57db5a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Sep 2023 10:56:44 +1000 Subject: [PATCH 511/512] Only list latest tested version --- docs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.rst b/docs/installation.rst index 724348c13..37739c8c0 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -500,7 +500,7 @@ These platforms have been reported to work at the versions mentioned. +==================================+===========================+==================+==============+ | macOS 13 Ventura | 3.8, 3.9, 3.10, 3.11 | 10.0.0 |arm | | +---------------------------+------------------+ | -| | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.5.0 | | +| | 3.7 | 9.5.0 | | +----------------------------------+---------------------------+------------------+--------------+ | macOS 12 Big Sur | 3.7, 3.8, 3.9, 3.10, 3.11 | 9.3.0 |arm | +----------------------------------+---------------------------+------------------+--------------+ From 20dbc0f074de4f710e1ccdc27a833acc72c8d295 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Sep 2023 11:37:22 +1000 Subject: [PATCH 512/512] Revert "Do not test PyQt6 on Python 3.12" This reverts commit 7a5ddc1712240b21d89581602acbb851c3897e4a. --- .ci/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/install.sh b/.ci/install.sh index 6e87d386d..d5cbd8248 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -42,7 +42,7 @@ if [[ $(uname) != CYGWIN* ]]; then if ! [ "$GHA_PYTHON_VERSION" == "3.12-dev" ]; then python3 -m pip install numpy ; fi # PyQt6 doesn't support PyPy3 - if [[ "$GHA_PYTHON_VERSION" != "3.12-dev" && $GHA_PYTHON_VERSION == 3.* ]]; then + if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 python3 -m pip install pyqt6 fi