From 700a8e98da37cb586e15fffeb46c04d755950f7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 28 Dec 2022 17:16:44 +1100 Subject: [PATCH 001/115] 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/115] 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/115] 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/115] 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/115] 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 7ad50d9185a7beef755ea7c19b8cfe6e5bea815e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 15 Jan 2023 19:42:55 -0600 Subject: [PATCH 006/115] log expected & actual color in image access tests --- Tests/test_image_access.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 6c4f1ceec..83eb7a1b2 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -139,15 +139,18 @@ class TestImageGetPixel(AccessTest): # check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), c) + d = im.getpixel((0, 0)) assert ( - im.getpixel((0, 0)) == c - ), f"put/getpixel roundtrip failed for mode {mode}, color {c}" + d == c + ), f"put/getpixel roundtrip failed for mode {mode}, expected {c} got {d}" # check putpixel negative index im.putpixel((-1, -1), c) - assert ( - im.getpixel((-1, -1)) == c - ), f"put/getpixel roundtrip negative index failed for mode {mode}, color {c}" + d = im.getpixel((-1, -1)) + assert d == c, ( + f"put/getpixel roundtrip negative index failed for mode {mode}, " + f"expected {c} got {d}" + ) # Check 0 im = Image.new(mode, (0, 0), None) @@ -166,13 +169,15 @@ class TestImageGetPixel(AccessTest): # check initial color im = Image.new(mode, (1, 1), c) - assert ( - im.getpixel((0, 0)) == c - ), f"initial color failed for mode {mode}, color {c} " + d = im.getpixel((0, 0)) + assert d == c, f"initial color failed for mode {mode}, expected {c} got {d}" + # check initial color negative index - assert ( - im.getpixel((-1, -1)) == c - ), f"initial color failed with negative index for mode {mode}, color {c} " + d = im.getpixel((-1, -1)) + assert d == c, ( + f"initial color failed with negative index for mode {mode}, " + f"expected {c} got {d}" + ) # Check 0 im = Image.new(mode, (0, 0), c) From 1603872f244da741feeac5a87d235e570723853f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 16 Jan 2023 07:46:11 -0600 Subject: [PATCH 007/115] 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 008/115] 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 009/115] 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 010/115] 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 011/115] [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 012/115] 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 b00bde977199968aa62c539b85ef3feb2338d080 Mon Sep 17 00:00:00 2001 From: Josh Ware Date: Thu, 19 Jan 2023 22:52:41 +1100 Subject: [PATCH 013/115] 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 9eacaee399acc4b61d420bd7edee6a83e03a7e07 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 28 Jan 2023 09:36:22 +1100 Subject: [PATCH 014/115] 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 015/115] 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 016/115] 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 017/115] 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 018/115] 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 019/115] 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 020/115] 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 021/115] 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 022/115] 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 023/115] 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 024/115] 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 5059e5c143ee4b9e625163897a5610611a325bef Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Feb 2023 08:11:50 +1100 Subject: [PATCH 025/115] 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 026/115] 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 027/115] 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 028/115] 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 029/115] 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 030/115] 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 031/115] [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 5c8a9165abcf7c2d2ec2829681e88726dafddaf4 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 23 Feb 2023 15:18:11 +0200 Subject: [PATCH 032/115] 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 033/115] 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 44c4e67fe13719672219aa2de67b8c19f921c191 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 24 Feb 2023 09:26:18 +0200 Subject: [PATCH 034/115] 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 1263018d2afbc19e9dce8b82d064cccc2e5ccfca Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 24 Feb 2023 23:00:29 +1100 Subject: [PATCH 035/115] 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: Fri, 3 Mar 2023 14:38:51 +1100 Subject: [PATCH 036/115] 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 037/115] 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 038/115] 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 039/115] 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 040/115] 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 041/115] 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 042/115] 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 043/115] 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 044/115] 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 045/115] 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 046/115] 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 047/115] 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 048/115] 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 049/115] 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 050/115] 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 051/115] 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 052/115] 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 053/115] 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 054/115] 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 055/115] 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 056/115] 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 057/115] 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 058/115] Added __int__ to IFDRational for Python >= 3.11 --- src/PIL/TiffImagePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 42038831c..8c0431492 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -425,6 +425,9 @@ class IFDRational(Rational): __ceil__ = _delegate("__ceil__") __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") + # Python >= 3.11 + if hasattr(Fraction, "__int__"): + __int__ = _delegate("__int__") class ImageFileDirectory_v2(MutableMapping): From a334bb6524d38916e39fad44e920ce91cc43eea4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Mar 2023 20:24:34 +1100 Subject: [PATCH 059/115] 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 060/115] 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 061/115] 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 062/115] 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 063/115] Close OleFileIO instance when closing or exiting FPX or MIC --- Tests/test_file_fpx.py | 10 ++++++++++ Tests/test_file_mic.py | 10 ++++++++++ src/PIL/FpxImagePlugin.py | 8 ++++++++ src/PIL/MicImagePlugin.py | 8 ++++++++ 4 files changed, 36 insertions(+) diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index fa22e90f6..9a1784d31 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -18,6 +18,16 @@ def test_sanity(): assert_image_equal_tofile(im, "Tests/images/input_bw_one_band.png") +def test_close(): + with Image.open("Tests/images/input_bw_one_band.fpx") as im: + pass + assert im.ole.fp.closed + + im = Image.open("Tests/images/input_bw_one_band.fpx") + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 464d138e2..2588d3a05 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -51,6 +51,16 @@ def test_seek(): assert im.tell() == 0 +def test_close(): + with Image.open(TEST_FILE) as im: + pass + assert im.ole.fp.closed + + im = Image.open(TEST_FILE) + im.close() + assert im.ole.fp.closed + + def test_invalid_file(): # Test an invalid OLE file invalid_file = "Tests/images/flower.jpg" diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index d145d01f7..2450c67e9 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -235,6 +235,14 @@ class FpxImageFile(ImageFile.ImageFile): return ImageFile.ImageFile.load(self) + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 8dd9f2909..58f7327bd 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -89,6 +89,14 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): def tell(self): return self.frame + def close(self): + self.ole.close() + super().close() + + def __exit__(self, *args): + self.ole.close() + super().__exit__() + # # -------------------------------------------------------------------- From 2fc7cfb6b256ef21ee7991604e972fd9e5e95efe Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sun, 12 Mar 2023 22:32:38 +1100 Subject: [PATCH 064/115] 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 065/115] 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 066/115] 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 067/115] 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 068/115] 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 069/115] 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 070/115] 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 071/115] 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 072/115] 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 073/115] 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 074/115] 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 075/115] 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 076/115] 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 077/115] 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 078/115] 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 079/115] 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 080/115] 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 081/115] [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 082/115] 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 083/115] 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 084/115] Allow libtiff_support_custom_tags to be missing --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 8c0431492..3d4d0910a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1807,7 +1807,7 @@ def _save(im, fp, filename): # Custom items are supported for int, float, unicode, string and byte # values. Other types and tuples require a tagtype. if tag not in TiffTags.LIBTIFF_CORE: - if not Image.core.libtiff_support_custom_tags: + if not getattr(Image.core, "libtiff_support_custom_tags", False): continue if tag in ifd.tagtype: From 16d235f8bc5055a961bfcad1006218171f305510 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Fri, 17 Mar 2023 18:08:19 +0000 Subject: [PATCH 085/115] Fix order of arguments in docstring --- docs/reference/ImageDraw.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/ImageDraw.rst b/docs/reference/ImageDraw.rst index 9565ab149..43a5a2bc2 100644 --- a/docs/reference/ImageDraw.rst +++ b/docs/reference/ImageDraw.rst @@ -320,8 +320,8 @@ Methods :param xy: Two points to define the bounding box. Sequence of either ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. .. versionadded:: 5.3.0 @@ -334,8 +334,8 @@ Methods ``[(x0, y0), (x1, y1)]`` or ``[x0, y0, x1, y1]``, where ``x1 >= x0`` and ``y1 >= y0``. The bounding box is inclusive of both endpoints. :param radius: Radius of the corners. - :param outline: Color to use for the outline. :param fill: Color to use for the fill. + :param outline: Color to use for the outline. :param width: The line width, in pixels. :param corners: A tuple of whether to round each corner, ``(top_left, top_right, bottom_right, bottom_left)``. From f03f9670fa986bba4c765440c129220bd5f1e006 Mon Sep 17 00:00:00 2001 From: Auto-5 <84275465+Auto-5@users.noreply.github.com> Date: Sat, 18 Mar 2023 14:12:53 +0000 Subject: [PATCH 086/115] 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 087/115] 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 088/115] 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 089/115] 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 090/115] 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 091/115] 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 092/115] PyUnicode_* may return NULL --- src/_imaging.c | 14 +++++++------- src/_imagingcms.c | 2 +- src/_imagingft.c | 4 ++-- src/_imagingmorph.c | 2 +- src/_webp.c | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 847eed5ce..96bfe5df7 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -4213,7 +4213,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpegVersion(void); PyObject *jpeglib_version = PyUnicode_FromString(ImagingJpegVersion()); PyDict_SetItemString(d, "jpeglib_version", jpeglib_version); - Py_DECREF(jpeglib_version); + Py_XDECREF(jpeglib_version); } #endif @@ -4222,7 +4222,7 @@ setup_module(PyObject *m) { extern const char *ImagingJpeg2KVersion(void); PyObject *jp2klib_version = PyUnicode_FromString(ImagingJpeg2KVersion()); PyDict_SetItemString(d, "jp2klib_version", jp2klib_version); - Py_DECREF(jp2klib_version); + Py_XDECREF(jp2klib_version); } #endif @@ -4233,7 +4233,7 @@ setup_module(PyObject *m) { #define tostr(a) tostr1(a) PyObject *libjpeg_turbo_version = PyUnicode_FromString(tostr(LIBJPEG_TURBO_VERSION)); PyDict_SetItemString(d, "libjpeg_turbo_version", libjpeg_turbo_version); - Py_DECREF(libjpeg_turbo_version); + Py_XDECREF(libjpeg_turbo_version); #undef tostr #undef tostr1 #else @@ -4249,7 +4249,7 @@ setup_module(PyObject *m) { extern const char *ImagingImageQuantVersion(void); PyObject *imagequant_version = PyUnicode_FromString(ImagingImageQuantVersion()); PyDict_SetItemString(d, "imagequant_version", imagequant_version); - Py_DECREF(imagequant_version); + Py_XDECREF(imagequant_version); } #else have_libimagequant = Py_False; @@ -4268,7 +4268,7 @@ setup_module(PyObject *m) { extern const char *ImagingZipVersion(void); PyObject *zlibversion = PyUnicode_FromString(ImagingZipVersion()); PyDict_SetItemString(d, "zlib_version", zlibversion); - Py_DECREF(zlibversion); + Py_XDECREF(zlibversion); } #endif @@ -4277,7 +4277,7 @@ setup_module(PyObject *m) { extern const char *ImagingTiffVersion(void); PyObject *libtiff_version = PyUnicode_FromString(ImagingTiffVersion()); PyDict_SetItemString(d, "libtiff_version", libtiff_version); - Py_DECREF(libtiff_version); + Py_XDECREF(libtiff_version); // Test for libtiff 4.0 or later, excluding libtiff 3.9.6 and 3.9.7 PyObject *support_custom_tags; @@ -4302,7 +4302,7 @@ setup_module(PyObject *m) { PyObject *pillow_version = PyUnicode_FromString(version); PyDict_SetItemString(d, "PILLOW_VERSION", pillow_version); - Py_DECREF(pillow_version); + Py_XDECREF(pillow_version); return 0; } diff --git a/src/_imagingcms.c b/src/_imagingcms.c index 779f31b9c..521151ae0 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -1534,7 +1534,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d", vn / 1000, (vn / 10) % 100); } PyDict_SetItemString(d, "littlecms_version", v); - Py_DECREF(v); + Py_XDECREF(v); return 0; } diff --git a/src/_imagingft.c b/src/_imagingft.c index 8697a74ff..beab7e0bd 100644 --- a/src/_imagingft.c +++ b/src/_imagingft.c @@ -1366,7 +1366,7 @@ setup_module(PyObject *m) { v = PyUnicode_FromFormat("%d.%d.%d", major, minor, patch); PyDict_SetItemString(d, "freetype2_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef HAVE_RAQM #if defined(HAVE_RAQM_SYSTEM) || defined(HAVE_FRIBIDI_SYSTEM) @@ -1392,7 +1392,7 @@ setup_module(PyObject *m) { v = Py_None; #endif PyDict_SetItemString(d, "raqm_version", v); - Py_DECREF(v); + Py_XDECREF(v); #ifdef FRIBIDI_MAJOR_VERSION { diff --git a/src/_imagingmorph.c b/src/_imagingmorph.c index 46a40e96d..7f85bc096 100644 --- a/src/_imagingmorph.c +++ b/src/_imagingmorph.c @@ -244,7 +244,7 @@ setup_module(PyObject *m) { PyObject *version = PyUnicode_FromString("0.1"); PyDict_SetItemString(d, "__version", version); - Py_DECREF(version); + Py_XDECREF(version); return 0; } diff --git a/src/_webp.c b/src/_webp.c index 5575e04f9..5c86341fa 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -964,7 +964,7 @@ setup_module(PyObject *m) { PyObject *webpdecoder_version = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", webpdecoder_version); - Py_DECREF(webpdecoder_version); + Py_XDECREF(webpdecoder_version); #ifdef HAVE_WEBPANIM /* Ready object types */ From 76d36da12e8b17d7e91814056c88bea5b0c614fa Mon Sep 17 00:00:00 2001 From: nulano Date: Tue, 21 Mar 2023 00:59:00 +0000 Subject: [PATCH 093/115] 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 094/115] 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 095/115] 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 096/115] 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 097/115] 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 098/115] 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 099/115] 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 100/115] 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 101/115] 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 102/115] 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 103/115] 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 104/115] 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 105/115] 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 106/115] 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 107/115] 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 108/115] 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 109/115] 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 110/115] 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 111/115] 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 112/115] 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 113/115] 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 114/115] 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 115/115] 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]